Commit fe80967b authored by Phil Hughes's avatar Phil Hughes

Merge branch '23206-load-participants-async' into 'master'

Load participants async

Closes #23206

See merge request gitlab-org/gitlab-ce!14836
parents 74a0e855 6dc9028f
...@@ -2,11 +2,8 @@ import Cookies from 'js-cookie'; ...@@ -2,11 +2,8 @@ import Cookies from 'js-cookie';
import bp from './breakpoints'; import bp from './breakpoints';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
const PARTICIPANTS_ROW_COUNT = 7;
export default class IssuableContext { export default class IssuableContext {
constructor(currentUser) { constructor(currentUser) {
this.initParticipants();
this.userSelect = new UsersSelect(currentUser); this.userSelect = new UsersSelect(currentUser);
$('select.select2').select2({ $('select.select2').select2({
...@@ -51,29 +48,4 @@ export default class IssuableContext { ...@@ -51,29 +48,4 @@ export default class IssuableContext {
} }
}); });
} }
initParticipants() {
$(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
return $('.js-participants-author').each(function forEachAuthor(i) {
if (i >= PARTICIPANTS_ROW_COUNT) {
$(this).addClass('js-participants-hidden').hide();
}
});
}
toggleHiddenParticipants() {
const currentText = $(this).text().trim();
const lessText = $(this).data('less-text');
const originalText = $(this).data('original-text');
if (currentText === originalText) {
$(this).text(lessText);
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} else {
$(this).text(originalText);
}
$('.js-participants-hidden').toggle();
}
} }
<script>
import { __, n__, sprintf } from '../../../locale';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
participants: {
type: Array,
required: false,
default: () => [],
},
numberOfLessParticipants: {
type: Number,
required: false,
default: 7,
},
},
data() {
return {
isShowingMoreParticipants: false,
};
},
components: {
loadingIcon,
userAvatarImage,
},
computed: {
lessParticipants() {
return this.participants.slice(0, this.numberOfLessParticipants);
},
visibleParticipants() {
return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
},
hasMoreParticipants() {
return this.participants.length > this.numberOfLessParticipants;
},
toggleLabel() {
let label = '';
if (this.isShowingMoreParticipants) {
label = __('- show less');
} else {
label = sprintf(__('+ %{moreCount} more'), {
moreCount: this.participants.length - this.numberOfLessParticipants,
});
}
return label;
},
participantLabel() {
return sprintf(
n__('%{count} participant', '%{count} participants', this.participants.length),
{ count: this.loading ? '' : this.participantCount },
);
},
participantCount() {
return this.participants.length;
},
},
methods: {
toggleMoreParticipants() {
this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
},
},
};
</script>
<template>
<div>
<div class="sidebar-collapsed-icon">
<i
class="fa fa-users"
aria-hidden="true">
</i>
<loading-icon
v-if="loading"
class="js-participants-collapsed-loading-icon" />
<span
v-else
class="js-participants-collapsed-count">
{{ participantCount }}
</span>
</div>
<div class="title hide-collapsed">
<loading-icon
v-if="loading"
:inline="true"
class="js-participants-expanded-loading-icon" />
{{ participantLabel }}
</div>
<div class="participants-list hide-collapsed">
<div
v-for="participant in visibleParticipants"
:key="participant.id"
class="participants-author js-participants-author">
<a
class="author_link"
:href="participant.web_url">
<user-avatar-image
:lazy="true"
:img-src="participant.avatar_url"
css-classes="avatar-inline"
:size="24"
:tooltip-text="participant.name"
tooltip-placement="bottom" />
</a>
</div>
</div>
<div
v-if="hasMoreParticipants"
class="participants-more hide-collapsed">
<button
type="button"
class="btn-transparent btn-blank js-toggle-participants-button"
@click="toggleMoreParticipants">
{{ toggleLabel }}
</button>
</div>
</div>
</template>
<script>
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import participants from './participants.vue';
export default {
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
components: {
participants,
},
};
</script>
<template>
<div class="block participants">
<participants
:loading="store.isFetching.participants"
:participants="store.participants"
:number-of-less-participants="7" />
</div>
</template>
<script>
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
import subscriptions from './subscriptions.vue';
export default {
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
components: {
subscriptions,
},
methods: {
onToggleSubscription() {
this.mediator.toggleSubscription()
.catch(() => {
Flash('Error occurred when toggling the notification subscription');
});
},
},
created() {
eventHub.$on('toggleSubscription', this.onToggleSubscription);
},
beforeDestroy() {
eventHub.$off('toggleSubscription', this.onToggleSubscription);
},
};
</script>
<template>
<div class="block subscriptions">
<subscriptions
:loading="store.isFetching.subscriptions"
:subscribed="store.subscribed" />
</div>
</template>
<script>
import { __ } from '../../../locale';
import eventHub from '../../event_hub';
import loadingButton from '../../../vue_shared/components/loading_button.vue';
export default {
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
subscribed: {
type: Boolean,
required: false,
},
},
components: {
loadingButton,
},
computed: {
buttonLabel() {
let label;
if (this.subscribed === false) {
label = __('Subscribe');
} else if (this.subscribed === true) {
label = __('Unsubscribe');
}
return label;
},
},
methods: {
toggleSubscription() {
eventHub.$emit('toggleSubscription');
},
},
};
</script>
<template>
<div>
<div class="sidebar-collapsed-icon">
<i
class="fa fa-rss"
aria-hidden="true">
</i>
</div>
<span class="issuable-header-text hide-collapsed pull-left">
{{ __('Notifications') }}
</span>
<loading-button
ref="loadingButton"
class="btn btn-default pull-right hide-collapsed js-issuable-subscribe-button"
:loading="loading"
:label="buttonLabel"
@click="toggleSubscription"
/>
</div>
</template>
...@@ -7,6 +7,7 @@ export default class SidebarService { ...@@ -7,6 +7,7 @@ export default class SidebarService {
constructor(endpointMap) { constructor(endpointMap) {
if (!SidebarService.singleton) { if (!SidebarService.singleton) {
this.endpoint = endpointMap.endpoint; this.endpoint = endpointMap.endpoint;
this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint;
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint; this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint; this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
...@@ -36,6 +37,10 @@ export default class SidebarService { ...@@ -36,6 +37,10 @@ export default class SidebarService {
}); });
} }
toggleSubscription() {
return Vue.http.post(this.toggleSubscriptionEndpoint);
}
moveIssue(moveToProjectId) { moveIssue(moveToProjectId) {
return Vue.http.post(this.moveIssueEndpoint, { return Vue.http.post(this.moveIssueEndpoint, {
move_to_project_id: moveToProjectId, move_to_project_id: moveToProjectId,
......
...@@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees'; ...@@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees';
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 LockIssueSidebar from './components/lock/lock_issue_sidebar.vue'; import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator'; import Mediator from './sidebar_mediator';
...@@ -49,6 +51,36 @@ function mountLockComponent(mediator) { ...@@ -49,6 +51,36 @@ function mountLockComponent(mediator) {
}).$mount(el); }).$mount(el);
} }
function mountParticipantsComponent() {
const el = document.querySelector('.js-sidebar-participants-entry-point');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
sidebarParticipants,
},
render: createElement => createElement('sidebar-participants', {}),
});
}
function mountSubscriptionsComponent() {
const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
sidebarSubscriptions,
},
render: createElement => createElement('sidebar-subscriptions', {}),
});
}
function domContentLoaded() { function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions); const mediator = new Mediator(sidebarOptions);
...@@ -63,6 +95,8 @@ function domContentLoaded() { ...@@ -63,6 +95,8 @@ function domContentLoaded() {
mountConfidentialComponent(mediator); mountConfidentialComponent(mediator);
mountLockComponent(mediator); mountLockComponent(mediator);
mountParticipantsComponent();
mountSubscriptionsComponent();
new SidebarMoveIssue( new SidebarMoveIssue(
mediator, mediator,
......
...@@ -8,6 +8,7 @@ export default class SidebarMediator { ...@@ -8,6 +8,7 @@ export default class SidebarMediator {
this.store = new Store(options); this.store = new Store(options);
this.service = new Service({ this.service = new Service({
endpoint: options.endpoint, endpoint: options.endpoint,
toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
moveIssueEndpoint: options.moveIssueEndpoint, moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint, projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
}); });
...@@ -39,10 +40,25 @@ export default class SidebarMediator { ...@@ -39,10 +40,25 @@ export default class SidebarMediator {
.then((data) => { .then((data) => {
this.store.setAssigneeData(data); this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data); this.store.setTimeTrackingData(data);
this.store.setParticipantsData(data);
this.store.setSubscriptionsData(data);
}) })
.catch(() => new Flash('Error occurred when fetching sidebar data')); .catch(() => new Flash('Error occurred when fetching sidebar data'));
} }
toggleSubscription() {
this.store.setFetchingState('subscriptions', true);
return this.service.toggleSubscription()
.then(() => {
this.store.setSubscribedState(!this.store.subscribed);
this.store.setFetchingState('subscriptions', false);
})
.catch((err) => {
this.store.setFetchingState('subscriptions', false);
throw err;
});
}
fetchAutocompleteProjects(searchTerm) { fetchAutocompleteProjects(searchTerm) {
return this.service.getProjectsAutocomplete(searchTerm) return this.service.getProjectsAutocomplete(searchTerm)
.then(response => response.json()) .then(response => response.json())
......
...@@ -12,10 +12,14 @@ export default class SidebarStore { ...@@ -12,10 +12,14 @@ export default class SidebarStore {
this.assignees = []; this.assignees = [];
this.isFetching = { this.isFetching = {
assignees: true, assignees: true,
participants: true,
subscriptions: true,
}; };
this.autocompleteProjects = []; this.autocompleteProjects = [];
this.moveToProjectId = 0; this.moveToProjectId = 0;
this.isLockDialogOpen = false; this.isLockDialogOpen = false;
this.participants = [];
this.subscribed = null;
SidebarStore.singleton = this; SidebarStore.singleton = this;
} }
...@@ -37,6 +41,20 @@ export default class SidebarStore { ...@@ -37,6 +41,20 @@ export default class SidebarStore {
this.humanTotalTimeSpent = data.human_total_time_spent; this.humanTotalTimeSpent = data.human_total_time_spent;
} }
setParticipantsData(data) {
this.isFetching.participants = false;
this.participants = data.participants || [];
}
setSubscriptionsData(data) {
this.isFetching.subscriptions = false;
this.subscribed = data.subscribed || false;
}
setFetchingState(key, value) {
this.isFetching[key] = value;
}
addAssignee(assignee) { addAssignee(assignee) {
if (!this.findAssignee(assignee)) { if (!this.findAssignee(assignee)) {
this.assignees.push(assignee); this.assignees.push(assignee);
...@@ -61,6 +79,10 @@ export default class SidebarStore { ...@@ -61,6 +79,10 @@ export default class SidebarStore {
this.autocompleteProjects = projects; this.autocompleteProjects = projects;
} }
setSubscribedState(subscribed) {
this.subscribed = subscribed;
}
setMoveToProjectId(moveToProjectId) { setMoveToProjectId(moveToProjectId) {
this.moveToProjectId = moveToProjectId; this.moveToProjectId = moveToProjectId;
} }
......
...@@ -11,7 +11,7 @@ export default class MRWidgetService { ...@@ -11,7 +11,7 @@ export default class MRWidgetService {
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath); this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath); this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath); this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
this.pollResource = Vue.resource(`${endpoints.statusPath}?basic=true`); this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath); this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
} }
......
...@@ -542,7 +542,9 @@ ...@@ -542,7 +542,9 @@
} }
.participants-list { .participants-list {
margin: -5px; display: flex;
flex-wrap: wrap;
margin: -7px;
} }
...@@ -553,7 +555,7 @@ ...@@ -553,7 +555,7 @@
.participants-author { .participants-author {
display: inline-block; display: inline-block;
padding: 5px; padding: 7px;
&:nth-of-type(7n) { &:nth-of-type(7n) {
padding-right: 0; padding-right: 0;
......
...@@ -74,7 +74,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -74,7 +74,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: serializer.represent(@issue) render json: serializer.represent(@issue, serializer: params[:serializer])
end end
end end
end end
......
...@@ -83,7 +83,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -83,7 +83,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000) Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: serializer.represent(@merge_request, basic: params[:basic]) render json: serializer.represent(@merge_request, serializer: params[:serializer])
end end
format.patch do format.patch do
......
...@@ -33,15 +33,17 @@ module IssuablesHelper ...@@ -33,15 +33,17 @@ module IssuablesHelper
end end
def serialize_issuable(issuable) def serialize_issuable(issuable)
case issuable serializer_klass = case issuable
when Issue when Issue
IssueSerializer.new(current_user: current_user, project: issuable.project).represent(issuable).to_json IssueSerializer
when MergeRequest when MergeRequest
MergeRequestSerializer MergeRequestSerializer
.new(current_user: current_user, project: issuable.project) end
.represent(issuable)
.to_json serializer_klass
end .new(current_user: current_user, project: issuable.project)
.represent(issuable)
.to_json
end end
def template_dropdown_tag(issuable, &block) def template_dropdown_tag(issuable, &block)
...@@ -357,7 +359,8 @@ module IssuablesHelper ...@@ -357,7 +359,8 @@ module IssuablesHelper
def issuable_sidebar_options(issuable, can_edit_issuable) def issuable_sidebar_options(issuable, can_edit_issuable)
{ {
endpoint: "#{issuable_json_path(issuable)}?basic=true", endpoint: "#{issuable_json_path(issuable)}?serializer=sidebar",
toggleSubscriptionEndpoint: toggle_subscription_path(issuable),
moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable), moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id), projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
editable: can_edit_issuable, editable: can_edit_issuable,
......
...@@ -13,6 +13,8 @@ module Subscribable ...@@ -13,6 +13,8 @@ module Subscribable
end end
def subscribed?(user, project = nil) def subscribed?(user, project = nil)
return false unless user
if subscription = subscriptions.find_by(user: user, project: project) if subscription = subscriptions.find_by(user: user, project: project)
subscription.subscribed subscription.subscribed
else else
......
class IssuableSidebarEntity < Grape::Entity
include RequestAwareEntity
expose :participants, using: ::API::Entities::UserBasic do |issuable|
issuable.participants(request.current_user)
end
expose :subscribed do |issuable|
issuable.subscribed?(request.current_user, issuable.project)
end
expose :time_estimate
expose :total_time_spent
expose :human_time_estimate
expose :human_total_time_spent
end
class IssueSerializer < BaseSerializer class IssueSerializer < BaseSerializer
entity IssueEntity # This overrided method takes care of which entity should be used
# to serialize the `issue` based on `basic` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def represent(merge_request, opts = {})
entity =
case opts[:serializer]
when 'sidebar'
IssueSidebarEntity
else
IssueEntity
end
super(merge_request, opts, entity)
end
end end
class IssueSidebarEntity < IssuableSidebarEntity
expose :assignees, using: API::Entities::UserBasic
end
class MergeRequestBasicEntity < Grape::Entity class MergeRequestBasicEntity < IssuableSidebarEntity
expose :assignee_id expose :assignee_id
expose :merge_status expose :merge_status
expose :merge_error expose :merge_error
expose :state expose :state
expose :source_branch_exists?, as: :source_branch_exists expose :source_branch_exists?, as: :source_branch_exists
expose :time_estimate
expose :total_time_spent
expose :human_time_estimate
expose :human_total_time_spent
end end
...@@ -3,7 +3,14 @@ class MergeRequestSerializer < BaseSerializer ...@@ -3,7 +3,14 @@ class MergeRequestSerializer < BaseSerializer
# to serialize the `merge_request` based on `basic` key in `opts` param. # to serialize the `merge_request` based on `basic` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope. # Hence, `entity` doesn't need to be declared on the class scope.
def represent(merge_request, opts = {}) def represent(merge_request, opts = {})
entity = opts[:basic] ? MergeRequestBasicEntity : MergeRequestEntity entity =
case opts[:serializer]
when 'basic', 'sidebar'
MergeRequestBasicEntity
else
MergeRequestEntity
end
super(merge_request, opts, entity) super(merge_request, opts, entity)
end end
end end
- participants_row = 7
- participants_size = participants.size
- participants_extra = participants_size - participants_row
.block.participants
.sidebar-collapsed-icon
= icon('users')
%span
= participants.count
.title.hide-collapsed
= pluralize participants.count, "participant"
.hide-collapsed.participants-list
- participants.each do |participant|
.participants-author.js-participants-author
= link_to_member(@project, participant, name: false, size: 24, lazy_load: true)
- if participants_extra > 0
.hide-collapsed.participants-more
%button.btn-transparent.btn-blank.js-participants-more{ type: 'button', data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
+ #{participants_extra} more
...@@ -123,17 +123,10 @@ ...@@ -123,17 +123,10 @@
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point #js-lock-entry-point
= render "shared/issuable/participants", participants: issuable.participants(current_user) .js-sidebar-participants-entry-point
- if current_user - if current_user
- subscribed = issuable.subscribed?(current_user, @project) .js-sidebar-subscriptions-entry-point
.block.light.subscription{ data: { url: toggle_subscription_path(issuable) } }
.sidebar-collapsed-icon
= icon('rss', 'aria-hidden': 'true')
%span.issuable-header-text.hide-collapsed.pull-left
Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
%button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
- project_ref = cross_project_reference(@project, issuable) - project_ref = cross_project_reference(@project, issuable)
.block.project-reference .block.project-reference
......
---
title: Update participants and subscriptions button in issuable sidebar to be async
merge_request: 14836
author:
type: changed
...@@ -20,11 +20,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -20,11 +20,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end end
step 'I should see that I am subscribed' do step 'I should see that I am subscribed' do
expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe' wait_for_requests
expect(find('.js-issuable-subscribe-button span')).to have_content 'Unsubscribe'
end end
step 'I should see that I am unsubscribed' do step 'I should see that I am unsubscribed' do
expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe' wait_for_requests
expect(find('.js-issuable-subscribe-button span')).to have_content 'Subscribe'
end end
step 'I click link "Closed"' do step 'I click link "Closed"' do
......
...@@ -83,15 +83,15 @@ describe Projects::MergeRequestsController do ...@@ -83,15 +83,15 @@ describe Projects::MergeRequestsController do
end end
describe 'as json' do describe 'as json' do
context 'with basic param' do context 'with basic serializer param' do
it 'renders basic MR entity as json' do it 'renders basic MR entity as json' do
go(basic: true, format: :json) go(serializer: 'basic', format: :json)
expect(response).to match_response_schema('entities/merge_request_basic') expect(response).to match_response_schema('entities/merge_request_basic')
end end
end end
context 'without basic param' do context 'without basic serializer param' do
it 'renders the merge request in the json format' do it 'renders the merge request in the json format' do
go(format: :json) go(format: :json)
......
...@@ -538,7 +538,7 @@ describe 'Issue Boards', :js do ...@@ -538,7 +538,7 @@ describe 'Issue Boards', :js do
end end
it 'does not show create new list' do it 'does not show create new list' do
expect(page).not_to have_selector('.js-new-board-list') expect(page).not_to have_button('.js-new-board-list')
end end
it 'does not allow dragging' do it 'does not allow dragging' do
......
...@@ -13,7 +13,7 @@ describe 'User manages subscription', :js do ...@@ -13,7 +13,7 @@ describe 'User manages subscription', :js do
end end
it 'toggles subscription' do it 'toggles subscription' do
subscribe_button = find('.issuable-subscribe-button span') subscribe_button = find('.js-issuable-subscribe-button')
expect(subscribe_button).to have_content('Subscribe') expect(subscribe_button).to have_content('Subscribe')
......
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"author_id": { "type": "integer" },
"description": { "type": ["string", "null"] },
"lock_version": { "type": ["string", "null"] },
"milestone_id": { "type": ["string", "null"] },
"title": { "type": "string" },
"moved_to_id": { "type": ["integer", "null"] },
"project_id": { "type": "integer" },
"web_url": { "type": "string" },
"state": { "type": "string" },
"create_note_path": { "type": "string" },
"preview_note_path": { "type": "string" },
"current_user": {
"type": "object",
"properties": {
"can_create_note": { "type": "boolean" },
"can_update": { "type": "boolean" }
}
},
"created_at": { "type": "date-time" },
"updated_at": { "type": "date-time" },
"branch_name": { "type": ["string", "null"] },
"due_date": { "type": "date" },
"confidential": { "type": "boolean" },
"discussion_locked": { "type": ["boolean", "null"] },
"updated_by_id": { "type": ["string", "null"] },
"deleted_at": { "type": ["string", "null"] },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["integer", "null"] },
"human_total_time_spent": { "type": ["integer", "null"] },
"milestone": { "type": ["object", "null"] },
"labels": {
"type": "array",
"items": { "$ref": "label.json" }
},
"assignees": { "type": ["array", "null"] }
},
"additionalProperties": false
}
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"subscribed": { "type": "boolean" },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["integer", "null"] },
"human_total_time_spent": { "type": ["integer", "null"] },
"participants": {
"type": "array",
"items": { "$ref": "../public_api/v4/user/basic.json" }
},
"assignees": {
"type": "array",
"items": { "$ref": "../public_api/v4/user/basic.json" }
}
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"id",
"color",
"description",
"title",
"priority"
],
"properties": {
"id": { "type": "integer" },
"color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
},
"description": { "type": ["string", "null"] },
"text_color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
},
"type": { "type": "string" },
"title": { "type": "string" },
"priority": { "type": ["integer", "null"] }
},
"additionalProperties": false
}
\ No newline at end of file
...@@ -9,7 +9,9 @@ ...@@ -9,7 +9,9 @@
"human_time_estimate": { "type": ["string", "null"] }, "human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] }, "human_total_time_spent": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] }, "merge_error": { "type": ["string", "null"] },
"assignee_id": { "type": ["integer", "null"] } "assignee_id": { "type": ["integer", "null"] },
"subscribed": { "type": ["boolean", "null"] },
"participants": { "type": "array" }
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -19,32 +19,7 @@ ...@@ -19,32 +19,7 @@
}, },
"labels": { "labels": {
"type": "array", "type": "array",
"items": { "items": { "$ref": "entities/label.json" }
"type": "object",
"required": [
"id",
"color",
"description",
"title",
"priority"
],
"properties": {
"id": { "type": "integer" },
"color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
},
"description": { "type": ["string", "null"] },
"text_color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
},
"type": { "type": "string" },
"title": { "type": "string" },
"priority": { "type": ["integer", "null"] }
},
"additionalProperties": false
}
}, },
"assignee": { "assignee": {
"id": { "type": "integet" }, "id": { "type": "integet" },
......
import $ from 'jquery';
import IssuableContext from '~/issuable_context';
describe('IssuableContext', () => {
describe('toggleHiddenParticipants', () => {
const event = jasmine.createSpyObj('event', ['preventDefault']);
beforeEach(() => {
spyOn($.fn, 'data').and.returnValue('data');
spyOn($.fn, 'text').and.returnValue('data');
});
afterEach(() => {
gl.lazyLoader = undefined;
});
it('calls loadCheck if lazyLoader is set', () => {
gl.lazyLoader = jasmine.createSpyObj('lazyLoader', ['loadCheck']);
IssuableContext.prototype.toggleHiddenParticipants(event);
expect(gl.lazyLoader.loadCheck).toHaveBeenCalled();
});
it('does not throw if lazyLoader is not defined', () => {
gl.lazyLoader = undefined;
const toggle = IssuableContext.prototype.toggleHiddenParticipants.bind(null, event);
expect(toggle).not.toThrow();
});
});
});
...@@ -109,12 +109,14 @@ const sidebarMockData = { ...@@ -109,12 +109,14 @@ const sidebarMockData = {
labels: [], labels: [],
web_url: '/root/some-project/issues/5', web_url: '/root/some-project/issues/5',
}, },
'/gitlab-org/gitlab-shell/issues/5/toggle_subscription': {},
}, },
}; };
export default { export default {
mediator: { mediator: {
endpoint: '/gitlab-org/gitlab-shell/issues/5.json', endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
editable: true, editable: true,
......
import Vue from 'vue';
import participants from '~/sidebar/components/participants/participants.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
const PARTICIPANT = {
id: 1,
state: 'active',
username: 'marcene',
name: 'Allie Will',
web_url: 'foo.com',
avatar_url: 'gravatar.com/avatar/xxx',
};
const PARTICIPANT_LIST = [
PARTICIPANT,
{ ...PARTICIPANT, id: 2 },
{ ...PARTICIPANT, id: 3 },
];
describe('Participants', function () {
let vm;
let Participants;
beforeEach(() => {
Participants = Vue.extend(participants);
});
afterEach(() => {
vm.$destroy();
});
describe('collapsed sidebar state', () => {
it('shows loading spinner when loading', () => {
vm = mountComponent(Participants, {
loading: true,
});
expect(vm.$el.querySelector('.js-participants-collapsed-loading-icon')).toBeDefined();
});
it('shows participant count when given', () => {
vm = mountComponent(Participants, {
loading: false,
participants: PARTICIPANT_LIST,
});
const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
});
it('shows full participant count when there are hidden participants', () => {
vm = mountComponent(Participants, {
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 1,
});
const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
});
});
describe('expanded sidebar state', () => {
it('shows loading spinner when loading', () => {
vm = mountComponent(Participants, {
loading: true,
});
expect(vm.$el.querySelector('.js-participants-expanded-loading-icon')).toBeDefined();
});
it('when only showing visible participants, shows an avatar only for each participant under the limit', (done) => {
const numberOfLessParticipants = 2;
vm = mountComponent(Participants, {
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants,
});
vm.isShowingMoreParticipants = false;
Vue.nextTick()
.then(() => {
const participantEls = vm.$el.querySelectorAll('.js-participants-author');
expect(participantEls.length).toBe(numberOfLessParticipants);
})
.then(done)
.catch(done.fail);
});
it('when only showing all participants, each has an avatar', (done) => {
const numberOfLessParticipants = 2;
vm = mountComponent(Participants, {
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants,
});
vm.isShowingMoreParticipants = true;
Vue.nextTick()
.then(() => {
const participantEls = vm.$el.querySelectorAll('.js-participants-author');
expect(participantEls.length).toBe(PARTICIPANT_LIST.length);
})
.then(done)
.catch(done.fail);
});
it('does not have more participants link when they can all be shown', () => {
const numberOfLessParticipants = 100;
vm = mountComponent(Participants, {
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants,
});
const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
expect(moreParticipantLink).toBeNull();
});
it('when too many participants, has more participants link to show more', (done) => {
vm = mountComponent(Participants, {
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 2,
});
vm.isShowingMoreParticipants = false;
Vue.nextTick()
.then(() => {
const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
expect(moreParticipantLink.textContent.trim()).toBe('+ 1 more');
})
.then(done)
.catch(done.fail);
});
it('when too many participants and already showing them, has more participants link to show less', (done) => {
vm = mountComponent(Participants, {
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 2,
});
vm.isShowingMoreParticipants = true;
Vue.nextTick()
.then(() => {
const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
expect(moreParticipantLink.textContent.trim()).toBe('- show less');
})
.then(done)
.catch(done.fail);
});
it('clicking more participants link emits event', () => {
vm = mountComponent(Participants, {
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 2,
});
const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
expect(vm.isShowingMoreParticipants).toBe(false);
moreParticipantLink.click();
expect(vm.isShowingMoreParticipants).toBe(true);
});
});
});
...@@ -57,8 +57,8 @@ describe('Sidebar mediator', () => { ...@@ -57,8 +57,8 @@ describe('Sidebar mediator', () => {
.then(() => { .then(() => {
expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm); expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm);
expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled(); expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled();
done();
}) })
.then(done)
.catch(done.fail); .catch(done.fail);
}); });
...@@ -72,8 +72,21 @@ describe('Sidebar mediator', () => { ...@@ -72,8 +72,21 @@ describe('Sidebar mediator', () => {
.then(() => { .then(() => {
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId); expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5'); expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
done();
}) })
.then(done)
.catch(done.fail);
});
it('toggle subscription', (done) => {
this.mediator.store.setSubscribedState(false);
spyOn(this.mediator.service, 'toggleSubscription').and.callThrough();
this.mediator.toggleSubscription()
.then(() => {
expect(this.mediator.service.toggleSubscription).toHaveBeenCalled();
expect(this.mediator.store.subscribed).toEqual(true);
})
.then(done)
.catch(done.fail); .catch(done.fail);
}); });
}); });
...@@ -7,6 +7,7 @@ describe('Sidebar service', () => { ...@@ -7,6 +7,7 @@ describe('Sidebar service', () => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor); Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
this.service = new SidebarService({ this.service = new SidebarService({
endpoint: '/gitlab-org/gitlab-shell/issues/5.json', endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
}); });
...@@ -23,6 +24,7 @@ describe('Sidebar service', () => { ...@@ -23,6 +24,7 @@ describe('Sidebar service', () => {
expect(resp).toBeDefined(); expect(resp).toBeDefined();
done(); done();
}) })
.then(done)
.catch(done.fail); .catch(done.fail);
}); });
...@@ -30,8 +32,8 @@ describe('Sidebar service', () => { ...@@ -30,8 +32,8 @@ describe('Sidebar service', () => {
this.service.update('issue[assignee_ids]', [1]) this.service.update('issue[assignee_ids]', [1])
.then((resp) => { .then((resp) => {
expect(resp).toBeDefined(); expect(resp).toBeDefined();
done();
}) })
.then(done)
.catch(done.fail); .catch(done.fail);
}); });
...@@ -39,8 +41,8 @@ describe('Sidebar service', () => { ...@@ -39,8 +41,8 @@ describe('Sidebar service', () => {
this.service.getProjectsAutocomplete() this.service.getProjectsAutocomplete()
.then((resp) => { .then((resp) => {
expect(resp).toBeDefined(); expect(resp).toBeDefined();
done();
}) })
.then(done)
.catch(done.fail); .catch(done.fail);
}); });
...@@ -48,8 +50,17 @@ describe('Sidebar service', () => { ...@@ -48,8 +50,17 @@ describe('Sidebar service', () => {
this.service.moveIssue(123) this.service.moveIssue(123)
.then((resp) => { .then((resp) => {
expect(resp).toBeDefined(); expect(resp).toBeDefined();
done();
}) })
.then(done)
.catch(done.fail);
});
it('toggles the subscription', (done) => {
this.service.toggleSubscription()
.then((resp) => {
expect(resp).toBeDefined();
})
.then(done)
.catch(done.fail); .catch(done.fail);
}); });
}); });
...@@ -2,21 +2,36 @@ import SidebarStore from '~/sidebar/stores/sidebar_store'; ...@@ -2,21 +2,36 @@ import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data'; import Mock from './mock_data';
import UsersMockHelper from '../helpers/user_mock_data_helper'; import UsersMockHelper from '../helpers/user_mock_data_helper';
describe('Sidebar store', () => { const ASSIGNEE = {
const assignee = { id: 2,
id: 2, name: 'gitlab user 2',
name: 'gitlab user 2', username: 'gitlab2',
username: 'gitlab2', avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', };
};
const ANOTHER_ASSINEE = {
const anotherAssignee = { id: 3,
id: 3, name: 'gitlab user 3',
name: 'gitlab user 3', username: 'gitlab3',
username: 'gitlab3', avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', };
};
const PARTICIPANT = {
id: 1,
state: 'active',
username: 'marcene',
name: 'Allie Will',
web_url: 'foo.com',
avatar_url: 'gravatar.com/avatar/xxx',
};
const PARTICIPANT_LIST = [
PARTICIPANT,
{ ...PARTICIPANT, id: 2 },
{ ...PARTICIPANT, id: 3 },
];
describe('Sidebar store', () => {
beforeEach(() => { beforeEach(() => {
this.store = new SidebarStore({ this.store = new SidebarStore({
currentUser: { currentUser: {
...@@ -40,23 +55,23 @@ describe('Sidebar store', () => { ...@@ -40,23 +55,23 @@ describe('Sidebar store', () => {
}); });
it('adds a new assignee', () => { it('adds a new assignee', () => {
this.store.addAssignee(assignee); this.store.addAssignee(ASSIGNEE);
expect(this.store.assignees.length).toEqual(1); expect(this.store.assignees.length).toEqual(1);
}); });
it('removes an assignee', () => { it('removes an assignee', () => {
this.store.removeAssignee(assignee); this.store.removeAssignee(ASSIGNEE);
expect(this.store.assignees.length).toEqual(0); expect(this.store.assignees.length).toEqual(0);
}); });
it('finds an existent assignee', () => { it('finds an existent assignee', () => {
let foundAssignee; let foundAssignee;
this.store.addAssignee(assignee); this.store.addAssignee(ASSIGNEE);
foundAssignee = this.store.findAssignee(assignee); foundAssignee = this.store.findAssignee(ASSIGNEE);
expect(foundAssignee).toBeDefined(); expect(foundAssignee).toBeDefined();
expect(foundAssignee).toEqual(assignee); expect(foundAssignee).toEqual(ASSIGNEE);
foundAssignee = this.store.findAssignee(anotherAssignee); foundAssignee = this.store.findAssignee(ANOTHER_ASSINEE);
expect(foundAssignee).toBeUndefined(); expect(foundAssignee).toBeUndefined();
}); });
...@@ -65,6 +80,28 @@ describe('Sidebar store', () => { ...@@ -65,6 +80,28 @@ describe('Sidebar store', () => {
expect(this.store.assignees.length).toEqual(0); expect(this.store.assignees.length).toEqual(0);
}); });
it('sets participants data', () => {
expect(this.store.participants.length).toEqual(0);
this.store.setParticipantsData({
participants: PARTICIPANT_LIST,
});
expect(this.store.isFetching.participants).toEqual(false);
expect(this.store.participants.length).toEqual(PARTICIPANT_LIST.length);
});
it('sets subcriptions data', () => {
expect(this.store.subscribed).toEqual(null);
this.store.setSubscriptionsData({
subscribed: true,
});
expect(this.store.isFetching.subscriptions).toEqual(false);
expect(this.store.subscribed).toEqual(true);
});
it('set assigned data', () => { it('set assigned data', () => {
const users = { const users = {
assignees: UsersMockHelper.createNumberRandomUsers(3), assignees: UsersMockHelper.createNumberRandomUsers(3),
...@@ -75,6 +112,14 @@ describe('Sidebar store', () => { ...@@ -75,6 +112,14 @@ describe('Sidebar store', () => {
expect(this.store.assignees.length).toEqual(3); expect(this.store.assignees.length).toEqual(3);
}); });
it('sets fetching state', () => {
expect(this.store.isFetching.participants).toEqual(true);
this.store.setFetchingState('participants', false);
expect(this.store.isFetching.participants).toEqual(false);
});
it('set time tracking data', () => { it('set time tracking data', () => {
this.store.setTimeTrackingData(Mock.time); this.store.setTimeTrackingData(Mock.time);
expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate); expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);
...@@ -90,6 +135,14 @@ describe('Sidebar store', () => { ...@@ -90,6 +135,14 @@ describe('Sidebar store', () => {
expect(this.store.autocompleteProjects).toEqual(projects); expect(this.store.autocompleteProjects).toEqual(projects);
}); });
it('sets subscribed state', () => {
expect(this.store.subscribed).toEqual(null);
this.store.setSubscribedState(true);
expect(this.store.subscribed).toEqual(true);
});
it('set move to project ID', () => { it('set move to project ID', () => {
const projectId = 7; const projectId = 7;
this.store.setMoveToProjectId(projectId); this.store.setMoveToProjectId(projectId);
......
import Vue from 'vue';
import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import eventHub from '~/sidebar/event_hub';
import mountComponent from '../helpers/vue_mount_component_helper';
import Mock from './mock_data';
describe('Sidebar Subscriptions', function () {
let vm;
let SidebarSubscriptions;
beforeEach(() => {
SidebarSubscriptions = Vue.extend(sidebarSubscriptions);
// Setup the stores, services, etc
// eslint-disable-next-line no-new
new SidebarMediator(Mock.mediator);
});
afterEach(() => {
vm.$destroy();
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
});
it('calls the mediator toggleSubscription on event', () => {
spyOn(SidebarMediator.prototype, 'toggleSubscription').and.returnValue(Promise.resolve());
vm = mountComponent(SidebarSubscriptions, {});
eventHub.$emit('toggleSubscription');
expect(SidebarMediator.prototype.toggleSubscription).toHaveBeenCalled();
});
});
import Vue from 'vue';
import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Subscriptions', function () {
let vm;
let Subscriptions;
beforeEach(() => {
Subscriptions = Vue.extend(subscriptions);
});
afterEach(() => {
vm.$destroy();
});
it('shows loading spinner when loading', () => {
vm = mountComponent(Subscriptions, {
loading: true,
subscribed: undefined,
});
expect(vm.$refs.loadingButton.loading).toBe(true);
expect(vm.$refs.loadingButton.label).toBeUndefined();
});
it('has "Subscribe" text when currently not subscribed', () => {
vm = mountComponent(Subscriptions, {
subscribed: false,
});
expect(vm.$refs.loadingButton.label).toBe('Subscribe');
});
it('has "Unsubscribe" text when currently not subscribed', () => {
vm = mountComponent(Subscriptions, {
subscribed: true,
});
expect(vm.$refs.loadingButton.label).toBe('Unsubscribe');
});
});
...@@ -6,6 +6,12 @@ describe Subscribable, 'Subscribable' do ...@@ -6,6 +6,12 @@ describe Subscribable, 'Subscribable' do
let(:user_1) { create(:user) } let(:user_1) { create(:user) }
describe '#subscribed?' do describe '#subscribed?' do
context 'without user' do
it 'returns false' do
expect(resource.subscribed?(nil, project)).to be_falsey
end
end
context 'without project' do context 'without project' do
it 'returns false when no subscription exists' do it 'returns false when no subscription exists' do
expect(resource.subscribed?(user_1)).to be_falsey expect(resource.subscribed?(user_1)).to be_falsey
......
require 'spec_helper'
describe IssueSerializer do
let(:resource) { create(:issue) }
let(:user) { create(:user) }
let(:json_entity) do
described_class.new(current_user: user)
.represent(resource, serializer: serializer)
.with_indifferent_access
end
context 'non-sidebar issue serialization' do
let(:serializer) { nil }
it 'matches issue json schema' do
expect(json_entity).to match_schema('entities/issue')
end
end
context 'sidebar issue serialization' do
let(:serializer) { 'sidebar' }
it 'matches sidebar issue json schema' do
expect(json_entity).to match_schema('entities/issue_sidebar')
end
end
end
...@@ -4,9 +4,13 @@ describe MergeRequestBasicSerializer do ...@@ -4,9 +4,13 @@ describe MergeRequestBasicSerializer do
let(:resource) { create(:merge_request) } let(:resource) { create(:merge_request) }
let(:user) { create(:user) } let(:user) { create(:user) }
subject { described_class.new.represent(resource) } let(:json_entity) do
described_class.new(current_user: user)
.represent(resource, serializer: 'basic')
.with_indifferent_access
end
it 'has important MergeRequest attributes' do it 'matches basic merge request json' do
expect(subject).to include(:merge_status) expect(json_entity).to match_schema('entities/merge_request_basic')
end end
end end
...@@ -9,11 +9,11 @@ describe MergeRequestSerializer do ...@@ -9,11 +9,11 @@ describe MergeRequestSerializer do
end end
describe '#represent' do describe '#represent' do
let(:opts) { { basic: basic } } let(:opts) { { serializer: serializer_entity } }
subject { serializer.represent(merge_request, basic: basic) } subject { serializer.represent(merge_request, serializer: serializer_entity) }
context 'when basic param is truthy' do context 'when passing basic serializer param' do
let(:basic) { true } let(:serializer_entity) { 'basic' }
it 'calls super class #represent with correct params' do it 'calls super class #represent with correct params' do
expect_any_instance_of(BaseSerializer).to receive(:represent) expect_any_instance_of(BaseSerializer).to receive(:represent)
...@@ -23,8 +23,8 @@ describe MergeRequestSerializer do ...@@ -23,8 +23,8 @@ describe MergeRequestSerializer do
end end
end end
context 'when basic param is falsy' do context 'when serializer param is falsy' do
let(:basic) { false } let(:serializer_entity) { nil }
it 'calls super class #represent with correct params' do it 'calls super class #represent with correct params' do
expect_any_instance_of(BaseSerializer).to receive(:represent) expect_any_instance_of(BaseSerializer).to receive(:represent)
......
require 'spec_helper'
require 'nokogiri'
describe 'shared/issuable/_participants.html.haml' do
let(:project) { create(:project) }
let(:participants) { create_list(:user, 100) }
before do
allow(view).to receive_messages(project: project,
participants: participants)
end
it 'renders lazy loaded avatars' do
render 'shared/issuable/participants'
html = Nokogiri::HTML(rendered)
avatars = html.css('.participants-author img')
avatars.each do |avatar|
expect(avatar[:class]).to include('lazy')
expect(avatar[:src]).to eql(LazyImageTagHelper.placeholder_image)
expect(avatar[:"data-src"]).to match('http://www.gravatar.com/avatar/')
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