Commit f00e6a7b authored by Samantha Ming's avatar Samantha Ming Committed by Bob Van Landuyt

Add vue components for reviewers in sidebar widget

Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/237921

- This is part 2 of 2.
- In this MR, we will be adding the necessary vue components of adding
reviewers in the sidebar widget.
parent 0d6c68bc
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import ReviewerAvatar from './reviewer_avatar.vue';
export default {
components: {
ReviewerAvatar,
},
props: {
user: {
type: Object,
required: true,
},
},
};
</script>
<template>
<button type="button" class="btn-link">
<reviewer-avatar :user="user" :img-size="24" />
<span class="author"> {{ user.name }} </span>
</button>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import CollapsedReviewer from './collapsed_reviewer.vue';
const DEFAULT_MAX_COUNTER = 99;
const DEFAULT_RENDER_COUNT = 5;
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
CollapsedReviewer,
GlIcon,
},
props: {
users: {
type: Array,
required: true,
},
},
computed: {
hasNoUsers() {
return !this.users.length;
},
hasMoreThanOneReviewer() {
return this.users.length > 1;
},
hasMoreThanTwoReviewers() {
return this.users.length > 2;
},
allReviewersCanMerge() {
return this.users.every(user => user.can_merge);
},
sidebarAvatarCounter() {
if (this.users.length > DEFAULT_MAX_COUNTER) {
return `${DEFAULT_MAX_COUNTER}+`;
}
return `+${this.users.length - 1}`;
},
collapsedUsers() {
const collapsedLength = this.hasMoreThanTwoReviewers ? 1 : this.users.length;
return this.users.slice(0, collapsedLength);
},
tooltipTitleMergeStatus() {
const mergeLength = this.users.filter(u => u.can_merge).length;
if (mergeLength === this.users.length) {
return '';
} else if (mergeLength > 0) {
return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
mergeLength,
usersLength: this.users.length,
});
}
return this.users.length === 1 ? __('cannot merge') : __('no one can merge');
},
tooltipTitle() {
const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length);
const renderUsers = this.users.slice(0, maxRender);
const names = renderUsers.map(u => u.name);
if (!this.users.length) {
return __('Reviewer(s)');
}
if (this.users.length > names.length) {
names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length }));
}
const text = names.join(', ');
return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text;
},
tooltipOptions() {
return { container: 'body', placement: 'left', boundary: 'viewport' };
},
},
};
</script>
<template>
<div
v-gl-tooltip="tooltipOptions"
:class="{ 'multiple-users': hasMoreThanOneReviewer }"
:title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user"
>
<gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" />
<collapsed-reviewer v-for="user in collapsedUsers" :key="user.id" :user="user" />
<button v-if="hasMoreThanTwoReviewers" class="btn-link" type="button">
<span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
<i
v-if="!allReviewersCanMerge"
aria-hidden="true"
class="fa fa-exclamation-triangle merge-icon"
></i>
</button>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { __, sprintf } from '~/locale';
export default {
props: {
user: {
type: Object,
required: true,
},
imgSize: {
type: Number,
required: true,
},
},
computed: {
reviewerAlt() {
return sprintf(__("%{userName}'s avatar"), { userName: this.user.name });
},
avatarUrl() {
return this.user.avatar || this.user.avatar_url || gon.default_avatar_url;
},
hasMergeIcon() {
return !this.user.can_merge;
},
},
};
</script>
<template>
<span class="position-relative">
<img
:alt="reviewerAlt"
:src="avatarUrl"
:width="imgSize"
:class="`s${imgSize}`"
class="avatar avatar-inline m-0"
data-qa-selector="avatar_image"
/>
<i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i>
</span>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ReviewerAvatar from './reviewer_avatar.vue';
export default {
components: {
ReviewerAvatar,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
user: {
type: Object,
required: true,
},
rootPath: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
default: 'bottom',
required: false,
},
tooltipHasName: {
type: Boolean,
default: true,
required: false,
},
issuableType: {
type: String,
default: 'issue',
required: false,
},
},
computed: {
cannotMerge() {
return this.issuableType === 'merge_request' && !this.user.can_merge;
},
tooltipTitle() {
if (this.cannotMerge && this.tooltipHasName) {
return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name });
} else if (this.cannotMerge) {
return __('Cannot merge');
} else if (this.tooltipHasName) {
return this.user.name;
}
return '';
},
tooltipOption() {
return {
container: 'body',
placement: this.tooltipPlacement,
boundary: 'viewport',
};
},
reviewerUrl() {
return this.user.web_url;
},
},
};
</script>
<template>
<!-- must be `d-inline-block` or parent flex-basis causes width issues -->
<gl-link
v-gl-tooltip="tooltipOption"
:href="reviewerUrl"
:title="tooltipTitle"
class="d-inline-block"
>
<!-- use d-flex so that slot can be appropriately styled -->
<span class="d-flex">
<reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
<slot :user="user"></slot>
</span>
</gl-link>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlLoadingIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
export default {
name: 'ReviewerTitle',
components: {
GlLoadingIcon,
},
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
numberOfReviewers: {
type: Number,
required: true,
},
editable: {
type: Boolean,
required: true,
},
showToggle: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
reviewerTitle() {
const reviewers = this.numberOfReviewers;
return n__('Reviewer', `%d Reviewers`, reviewers);
},
},
};
</script>
<template>
<div class="title hide-collapsed">
{{ reviewerTitle }}
<gl-loading-icon v-if="loading" inline class="align-bottom" />
<a
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="reviewer"
>
{{ __('Edit') }}
</a>
<a
v-if="showToggle"
:aria-label="__('Toggle sidebar')"
class="gutter-toggle float-right js-sidebar-toggle"
href="#"
role="button"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
</a>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import CollapsedReviewerList from './collapsed_reviewer_list.vue';
import UncollapsedReviewerList from './uncollapsed_reviewer_list.vue';
export default {
// name: 'Reviewers' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Reviewers',
components: {
CollapsedReviewerList,
UncollapsedReviewerList,
},
props: {
rootPath: {
type: String,
required: true,
},
users: {
type: Array,
required: true,
},
editable: {
type: Boolean,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
},
computed: {
hasNoUsers() {
return !this.users.length;
},
sortedReviewers() {
const canMergeUsers = this.users.filter(user => user.can_merge);
const canNotMergeUsers = this.users.filter(user => !user.can_merge);
return [...canMergeUsers, ...canNotMergeUsers];
},
},
methods: {
assignSelf() {
this.$emit('assign-self');
},
},
};
</script>
<template>
<div>
<collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" />
<div class="value hide-collapsed">
<template v-if="hasNoUsers">
<span class="assign-yourself no-value qa-assign-yourself">
{{ __('None') }}
</span>
</template>
<uncollapsed-reviewer-list
v-else
:users="sortedReviewers"
:root-path="rootPath"
:issuable-type="issuableType"
/>
</div>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { deprecatedCreateFlash as Flash } from '~/flash';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReviewerTitle from './reviewer_title.vue';
import Reviewers from './reviewers.vue';
import { __ } from '~/locale';
export default {
name: 'SidebarReviewers',
components: {
ReviewerTitle,
Reviewers,
},
mixins: [glFeatureFlagsMixin()],
props: {
mediator: {
type: Object,
required: true,
},
field: {
type: String,
required: true,
},
signedIn: {
type: Boolean,
required: false,
default: false,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
issuableIid: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
},
data() {
return {
store: new Store(),
loading: false,
};
},
created() {
this.removeReviewer = this.store.removeReviewer.bind(this.store);
this.addReviewer = this.store.addReviewer.bind(this.store);
this.removeAllReviewers = this.store.removeAllReviewers.bind(this.store);
// Get events from deprecatedJQueryDropdown
eventHub.$on('sidebar.removeReviewer', this.removeReviewer);
eventHub.$on('sidebar.addReviewer', this.addReviewer);
eventHub.$on('sidebar.removeAllReviewers', this.removeAllReviewers);
eventHub.$on('sidebar.saveReviewers', this.saveReviewers);
},
beforeDestroy() {
eventHub.$off('sidebar.removeReviewer', this.removeReviewer);
eventHub.$off('sidebar.addReviewer', this.addReviewer);
eventHub.$off('sidebar.removeAllReviewers', this.removeAllReviewers);
eventHub.$off('sidebar.saveReviewers', this.saveReviewers);
},
methods: {
saveReviewers() {
this.loading = true;
this.mediator
.saveReviewers(this.field)
.then(() => {
this.loading = false;
// Uncomment once this issue has been addressed > https://gitlab.com/gitlab-org/gitlab/-/issues/237922
// refreshUserMergeRequestCounts();
})
.catch(() => {
this.loading = false;
return new Flash(__('Error occurred when saving reviewers'));
});
},
},
};
</script>
<template>
<div>
<reviewer-title
:number-of-reviewers="store.reviewers.length"
:loading="loading || store.isFetching.reviewers"
:editable="store.editable"
:show-toggle="!signedIn"
/>
<reviewers
v-if="!store.isFetching.reviewers"
:root-path="store.rootPath"
:users="store.reviewers"
:editable="store.editable"
:issuable-type="issuableType"
class="value"
/>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { __, sprintf } from '~/locale';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
ReviewerAvatarLink,
},
props: {
users: {
type: Array,
required: true,
},
rootPath: {
type: String,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
},
data() {
return {
showLess: true,
};
},
computed: {
firstUser() {
return this.users[0];
},
hasOneUser() {
return this.users.length === 1;
},
hiddenReviewersLabel() {
const { numberOfHiddenReviewers } = this;
return sprintf(__('+ %{numberOfHiddenReviewers} more'), { numberOfHiddenReviewers });
},
renderShowMoreSection() {
return this.users.length > DEFAULT_RENDER_COUNT;
},
numberOfHiddenReviewers() {
return this.users.length - DEFAULT_RENDER_COUNT;
},
uncollapsedUsers() {
const uncollapsedLength = this.showLess
? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
: this.users.length;
return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
},
username() {
return `@${this.firstUser.username}`;
},
},
methods: {
toggleShowLess() {
this.showLess = !this.showLess;
},
},
};
</script>
<template>
<reviewer-avatar-link
v-if="hasOneUser"
#default="{ user }"
tooltip-placement="left"
:tooltip-has-name="false"
:user="firstUser"
:root-path="rootPath"
:issuable-type="issuableType"
>
<div class="ml-2">
<span class="author"> {{ user.name }} </span>
<span class="username"> {{ username }} </span>
</div>
</reviewer-avatar-link>
<div v-else>
<div class="user-list">
<div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more">
<button
type="button"
class="btn-link"
data-qa-selector="more_reviewers_link"
@click="toggleShowLess"
>
<template v-if="showLess">
{{ hiddenReviewersLabel }}
</template>
<template v-else>{{ __('- show less') }}</template>
</button>
</div>
</div>
</template>
......@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarLabels from './components/labels/sidebar_labels.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
......@@ -56,6 +57,36 @@ function mountAssigneesComponent(mediator) {
});
}
function mountReviewersComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-reviewers');
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
if (!el) return;
const { iid, fullPath } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
apolloProvider,
components: {
SidebarReviewers,
},
render: createElement =>
createElement('sidebar-reviewers', {
props: {
mediator,
issuableIid: String(iid),
projectPath: fullPath,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
issuableType: isInIssuePage() ? 'issue' : 'merge_request',
},
}),
});
}
export function mountSidebarLabels() {
const el = document.querySelector('.js-sidebar-labels');
......@@ -245,6 +276,7 @@ function mountSeverityComponent() {
export function mountSidebar(mediator) {
mountAssigneesComponent(mediator);
mountReviewersComponent(mediator);
mountConfidentialComponent(mediator);
mountLockComponent();
mountParticipantsComponent(mediator);
......
......@@ -117,7 +117,8 @@
}
}
.assignee {
.assignee,
.reviewer {
.merge-icon {
color: $orange-400;
position: absolute;
......
......@@ -5,6 +5,7 @@
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
- reviewers = local_assigns.fetch(:reviewers, nil)
- if Feature.enabled?(:vue_issuable_sidebar, @project.group)
%aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in,
......@@ -28,6 +29,10 @@
.block.assignee.qa-assignee-block
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees
- if Feature.enabled?(:merge_request_reviewers, @project) && reviewers
.block.reviewer.qa-reviewer-block
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
- if issuable_sidebar[:supports_milestone]
......
......@@ -977,6 +977,9 @@ msgstr ""
msgid "+ %{numberOfHiddenAssignees} more"
msgstr ""
msgid "+ %{numberOfHiddenReviewers} more"
msgstr ""
msgid "+%d more"
msgid_plural "+%d more"
msgstr[0] ""
......@@ -10223,6 +10226,9 @@ msgstr ""
msgid "Error occurred when saving assignees"
msgstr ""
msgid "Error occurred when saving reviewers"
msgstr ""
msgid "Error occurred when toggling the notification subscription"
msgstr ""
......@@ -21987,6 +21993,11 @@ msgid "ReviewApp|Enable Review App"
msgstr ""
msgid "Reviewer"
msgid_plural "%d Reviewers"
msgstr[0] ""
msgstr[1] ""
msgid "Reviewer(s)"
msgstr ""
msgid "Reviewing"
......
......@@ -21,24 +21,6 @@ RSpec.describe 'Merge request > User edits MR' do
it_behaves_like 'an editable merge request'
end
context 'when merge_request_reviewers is turned on' do
before do
stub_feature_flags(merge_request_reviewers: true)
end
context 'non-fork merge request' do
include_context 'merge request edit context'
it_behaves_like 'an editable merge request with reviewers'
end
context 'for a forked project' do
let(:source_project) { fork_project(target_project, nil, repository: true) }
include_context 'merge request edit context'
it_behaves_like 'an editable merge request with reviewers'
end
end
context 'when merge_request_reviewers is turned off' do
before do
stub_feature_flags(merge_request_reviewers: false)
......
......@@ -24,6 +24,23 @@ RSpec.describe 'User views an open merge request' do
expect(page).to have_content(merge_request.title)
end
it 'has reviewers in sidebar' do
expect(page).to have_css('.reviewer')
end
end
context 'when merge_request_reviewers is turned off' do
let(:project) { create(:project, :public, :repository) }
before do
stub_feature_flags(merge_request_reviewers: false)
visit(merge_request_path(merge_request))
end
it 'has reviewers in sidebar' do
expect(page).not_to have_css('.reviewer')
end
end
context 'when a merge request has repository', :js do
......
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import Component from '~/sidebar/components/reviewers/reviewer_title.vue';
describe('ReviewerTitle component', () => {
let wrapper;
const createComponent = props => {
return shallowMount(Component, {
propsData: {
numberOfReviewers: 0,
editable: false,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('reviewer title', () => {
it('renders reviewer', () => {
wrapper = createComponent({
numberOfReviewers: 1,
editable: false,
});
expect(wrapper.vm.$el.innerText.trim()).toEqual('Reviewer');
});
it('renders 2 reviewers', () => {
wrapper = createComponent({
numberOfReviewers: 2,
editable: false,
});
expect(wrapper.vm.$el.innerText.trim()).toEqual('2 Reviewers');
});
});
describe('gutter toggle', () => {
it('does not show toggle by default', () => {
wrapper = createComponent({
numberOfReviewers: 2,
editable: false,
});
expect(wrapper.vm.$el.querySelector('.gutter-toggle')).toBeNull();
});
it('shows toggle when showToggle is true', () => {
wrapper = createComponent({
numberOfReviewers: 2,
editable: false,
showToggle: true,
});
expect(wrapper.vm.$el.querySelector('.gutter-toggle')).toEqual(expect.any(Object));
});
});
it('does not render spinner by default', () => {
wrapper = createComponent({
numberOfReviewers: 0,
editable: false,
});
expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
});
it('renders spinner when loading', () => {
wrapper = createComponent({
loading: true,
numberOfReviewers: 0,
editable: false,
});
expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy();
});
it('does not render edit link when not editable', () => {
wrapper = createComponent({
numberOfReviewers: 0,
editable: false,
});
expect(wrapper.vm.$el.querySelector('.edit-link')).toBeNull();
});
it('renders edit link when editable', () => {
wrapper = createComponent({
numberOfReviewers: 0,
editable: true,
});
expect(wrapper.vm.$el.querySelector('.edit-link')).not.toBeNull();
});
it('tracks the event when edit is clicked', () => {
wrapper = createComponent({
numberOfReviewers: 0,
editable: true,
});
const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent('.js-sidebar-dropdown-toggle');
expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
label: 'right_sidebar',
property: 'reviewer',
});
});
});
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import { GlIcon } from '@gitlab/ui';
import Reviewer from '~/sidebar/components/reviewers/reviewers.vue';
import UsersMock from './mock_data';
import UsersMockHelper from '../helpers/user_mock_data_helper';
describe('Reviewer component', () => {
const getDefaultProps = () => ({
rootPath: 'http://localhost:3000',
users: [],
editable: false,
});
let wrapper;
const createWrapper = (propsData = getDefaultProps()) => {
wrapper = mount(Reviewer, {
propsData,
});
};
const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *');
afterEach(() => {
wrapper.destroy();
});
describe('No reviewers/users', () => {
it('displays no reviewer icon when collapsed', () => {
createWrapper();
const collapsedChildren = findCollapsedChildren();
const userIcon = collapsedChildren.at(0).find(GlIcon);
expect(collapsedChildren.length).toBe(1);
expect(collapsedChildren.at(0).attributes('aria-label')).toBe('None');
expect(userIcon.exists()).toBe(true);
expect(userIcon.props('name')).toBe('user');
});
});
describe('One reviewer/user', () => {
it('displays one reviewer icon when collapsed', () => {
createWrapper({
...getDefaultProps(),
users: [UsersMock.user],
});
const collapsedChildren = findCollapsedChildren();
const reviewer = collapsedChildren.at(0);
expect(collapsedChildren.length).toBe(1);
expect(reviewer.find('.avatar').attributes('src')).toBe(UsersMock.user.avatar);
expect(reviewer.find('.avatar').attributes('alt')).toBe(`${UsersMock.user.name}'s avatar`);
expect(trimText(reviewer.find('.author').text())).toBe(UsersMock.user.name);
});
});
describe('Two or more reviewers/users', () => {
it('displays two reviewer icons when collapsed', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
createWrapper({
...getDefaultProps(),
users,
});
const collapsedChildren = findCollapsedChildren();
expect(collapsedChildren.length).toBe(2);
const first = collapsedChildren.at(0);
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url);
expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
expect(trimText(first.find('.author').text())).toBe(users[0].name);
const second = collapsedChildren.at(1);
expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar_url);
expect(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`);
expect(trimText(second.find('.author').text())).toBe(users[1].name);
});
it('displays one reviewer icon and counter when collapsed', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
createWrapper({
...getDefaultProps(),
users,
});
const collapsedChildren = findCollapsedChildren();
expect(collapsedChildren.length).toBe(2);
const first = collapsedChildren.at(0);
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url);
expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
expect(trimText(first.find('.author').text())).toBe(users[0].name);
const second = collapsedChildren.at(1);
expect(trimText(second.find('.avatar-counter').text())).toBe('+2');
});
it('Shows two reviewers', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
createWrapper({
...getDefaultProps(),
users,
editable: true,
});
expect(wrapper.findAll('.user-item').length).toBe(users.length);
expect(wrapper.find('.user-list-more').exists()).toBe(false);
});
it('shows sorted reviewer where "can merge" users are sorted first', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = false;
users[1].can_merge = false;
users[2].can_merge = true;
createWrapper({
...getDefaultProps(),
users,
editable: true,
});
expect(wrapper.vm.sortedReviewers[0].can_merge).toBe(true);
});
it('passes the sorted reviewers to the uncollapsed-reviewer-list', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = false;
users[1].can_merge = false;
users[2].can_merge = true;
createWrapper({
...getDefaultProps(),
users,
});
const userItems = wrapper.findAll('.user-list .user-item a');
expect(userItems.length).toBe(3);
expect(userItems.at(0).attributes('title')).toBe(users[2].name);
});
it('passes the sorted reviewers to the collapsed-reviewer-list', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = false;
users[1].can_merge = false;
users[2].can_merge = true;
createWrapper({
...getDefaultProps(),
users,
});
const collapsedButton = wrapper.find('.sidebar-collapsed-user button');
expect(trimText(collapsedButton.text())).toBe(users[2].name);
});
});
});
......@@ -11,6 +11,15 @@ RSpec.shared_examples 'an editable merge request' do
expect(page).to have_content user.name
end
find('.js-reviewer-search').click
page.within '.dropdown-menu-user' do
click_link user.name
end
expect(find('input[name="merge_request[reviewer_ids][]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-reviewer-search' do
expect(page).to have_content user.name
end
click_button 'Milestone'
page.within '.issue-milestone' do
click_link milestone.title
......@@ -38,6 +47,10 @@ RSpec.shared_examples 'an editable merge request' do
expect(page).to have_content user.name
end
page.within '.reviewer' do
expect(page).to have_content user.name
end
page.within '.milestone' do
expect(page).to have_content milestone.title
end
......@@ -124,16 +137,3 @@ end
def get_textarea_height
page.evaluate_script('document.getElementById("merge_request_description").offsetHeight')
end
RSpec.shared_examples 'an editable merge request with reviewers' do
it 'updates merge request', :js do
find('.js-reviewer-search').click
page.within '.dropdown-menu-user' do
click_link user.name
end
expect(find('input[name="merge_request[reviewer_ids][]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-reviewer-search' do
expect(page).to have_content user.name
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment