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'; ...@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarLabels from './components/labels/sidebar_labels.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 ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue'; import SidebarMoveIssue from './lib/sidebar_move_issue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue'; import IssuableLockForm from './components/lock/issuable_lock_form.vue';
...@@ -56,6 +57,36 @@ function mountAssigneesComponent(mediator) { ...@@ -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() { export function mountSidebarLabels() {
const el = document.querySelector('.js-sidebar-labels'); const el = document.querySelector('.js-sidebar-labels');
...@@ -245,6 +276,7 @@ function mountSeverityComponent() { ...@@ -245,6 +276,7 @@ function mountSeverityComponent() {
export function mountSidebar(mediator) { export function mountSidebar(mediator) {
mountAssigneesComponent(mediator); mountAssigneesComponent(mediator);
mountReviewersComponent(mediator);
mountConfidentialComponent(mediator); mountConfidentialComponent(mediator);
mountLockComponent(); mountLockComponent();
mountParticipantsComponent(mediator); mountParticipantsComponent(mediator);
......
...@@ -117,7 +117,8 @@ ...@@ -117,7 +117,8 @@
} }
} }
.assignee { .assignee,
.reviewer {
.merge-icon { .merge-icon {
color: $orange-400; color: $orange-400;
position: absolute; position: absolute;
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
- signed_in = !!issuable_sidebar.dig(:current_user, :id) - signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit) - can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras" - 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) - if Feature.enabled?(:vue_issuable_sidebar, @project.group)
%aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in, %aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in,
...@@ -28,6 +29,10 @@ ...@@ -28,6 +29,10 @@
.block.assignee.qa-assignee-block .block.assignee.qa-assignee-block
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees = 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 = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
- if issuable_sidebar[:supports_milestone] - if issuable_sidebar[:supports_milestone]
......
...@@ -977,6 +977,9 @@ msgstr "" ...@@ -977,6 +977,9 @@ msgstr ""
msgid "+ %{numberOfHiddenAssignees} more" msgid "+ %{numberOfHiddenAssignees} more"
msgstr "" msgstr ""
msgid "+ %{numberOfHiddenReviewers} more"
msgstr ""
msgid "+%d more" msgid "+%d more"
msgid_plural "+%d more" msgid_plural "+%d more"
msgstr[0] "" msgstr[0] ""
...@@ -10223,6 +10226,9 @@ msgstr "" ...@@ -10223,6 +10226,9 @@ msgstr ""
msgid "Error occurred when saving assignees" msgid "Error occurred when saving assignees"
msgstr "" msgstr ""
msgid "Error occurred when saving reviewers"
msgstr ""
msgid "Error occurred when toggling the notification subscription" msgid "Error occurred when toggling the notification subscription"
msgstr "" msgstr ""
...@@ -21987,6 +21993,11 @@ msgid "ReviewApp|Enable Review App" ...@@ -21987,6 +21993,11 @@ msgid "ReviewApp|Enable Review App"
msgstr "" msgstr ""
msgid "Reviewer" msgid "Reviewer"
msgid_plural "%d Reviewers"
msgstr[0] ""
msgstr[1] ""
msgid "Reviewer(s)"
msgstr "" msgstr ""
msgid "Reviewing" msgid "Reviewing"
......
...@@ -21,24 +21,6 @@ RSpec.describe 'Merge request > User edits MR' do ...@@ -21,24 +21,6 @@ RSpec.describe 'Merge request > User edits MR' do
it_behaves_like 'an editable merge request' it_behaves_like 'an editable merge request'
end 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 context 'when merge_request_reviewers is turned off' do
before do before do
stub_feature_flags(merge_request_reviewers: false) stub_feature_flags(merge_request_reviewers: false)
......
...@@ -24,6 +24,23 @@ RSpec.describe 'User views an open merge request' do ...@@ -24,6 +24,23 @@ RSpec.describe 'User views an open merge request' do
expect(page).to have_content(merge_request.title) expect(page).to have_content(merge_request.title)
end 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 end
context 'when a merge request has repository', :js do 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 ...@@ -11,6 +11,15 @@ RSpec.shared_examples 'an editable merge request' do
expect(page).to have_content user.name expect(page).to have_content user.name
end 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' click_button 'Milestone'
page.within '.issue-milestone' do page.within '.issue-milestone' do
click_link milestone.title click_link milestone.title
...@@ -38,6 +47,10 @@ RSpec.shared_examples 'an editable merge request' do ...@@ -38,6 +47,10 @@ RSpec.shared_examples 'an editable merge request' do
expect(page).to have_content user.name expect(page).to have_content user.name
end end
page.within '.reviewer' do
expect(page).to have_content user.name
end
page.within '.milestone' do page.within '.milestone' do
expect(page).to have_content milestone.title expect(page).to have_content milestone.title
end end
...@@ -124,16 +137,3 @@ end ...@@ -124,16 +137,3 @@ end
def get_textarea_height def get_textarea_height
page.evaluate_script('document.getElementById("merge_request_description").offsetHeight') page.evaluate_script('document.getElementById("merge_request_description").offsetHeight')
end 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