Commit 95f4f26a authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Bob Van Landuyt

Set user availability from profile settings

Allows users to set their user availability
status from the profile settings page
and set status modal

Set busy status from status modal
parent 0a719f47
...@@ -566,12 +566,13 @@ const Api = { ...@@ -566,12 +566,13 @@ const Api = {
}); });
}, },
postUserStatus({ emoji, message }) { postUserStatus({ emoji, message, availability }) {
const url = Api.buildUrl(this.userPostStatusPath); const url = Api.buildUrl(this.userPostStatusPath);
return axios.put(url, { return axios.put(url, {
emoji, emoji,
message, message,
availability,
}); });
}, },
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility'; import { highCountTrim } from '~/lib/utils/text_utility';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
...@@ -34,26 +35,45 @@ function initStatusTriggers() { ...@@ -34,26 +35,45 @@ function initStatusTriggers() {
const statusModalElement = document.createElement('div'); const statusModalElement = document.createElement('div');
setStatusModalWrapperEl.appendChild(statusModalElement); setStatusModalWrapperEl.appendChild(statusModalElement);
Vue.use(GlToast);
Vue.use(Translate); Vue.use(Translate);
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: statusModalElement, el: statusModalElement,
data() { data() {
const { currentEmoji, currentMessage } = setStatusModalWrapperEl.dataset; const {
currentEmoji,
defaultEmoji,
currentMessage,
currentAvailability,
canSetUserAvailability,
} = setStatusModalWrapperEl.dataset;
return { return {
currentEmoji, currentEmoji,
defaultEmoji,
currentMessage, currentMessage,
currentAvailability,
canSetUserAvailability,
}; };
}, },
render(createElement) { render(createElement) {
const { currentEmoji, currentMessage } = this; const {
currentEmoji,
defaultEmoji,
currentMessage,
currentAvailability,
canSetUserAvailability,
} = this;
return createElement(SetStatusModalWrapper, { return createElement(SetStatusModalWrapper, {
props: { props: {
currentEmoji, currentEmoji,
defaultEmoji,
currentMessage, currentMessage,
currentAvailability,
canSetUserAvailability,
}, },
}); });
}, },
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* eslint-disable vue/no-v-html */ /* eslint-disable vue/no-v-html */
import $ from 'jquery'; import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { GlModal, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { GlModal, GlTooltipDirective, GlIcon, GlFormCheckbox } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import Api from '~/api'; import Api from '~/api';
...@@ -11,16 +11,26 @@ import { isUserBusy, isValidAvailibility } from './utils'; ...@@ -11,16 +11,26 @@ import { isUserBusy, isValidAvailibility } from './utils';
import * as Emoji from '~/emoji'; import * as Emoji from '~/emoji';
const emojiMenuClass = 'js-modal-status-emoji-menu'; const emojiMenuClass = 'js-modal-status-emoji-menu';
export const AVAILABILITY_STATUS = {
BUSY: 'busy',
NOT_SET: 'not_set',
};
export default { export default {
components: { components: {
GlIcon, GlIcon,
GlModal, GlModal,
GlFormCheckbox,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
defaultEmoji: {
type: String,
required: false,
default: '',
},
currentEmoji: { currentEmoji: {
type: String, type: String,
required: true, required: true,
...@@ -55,8 +65,11 @@ export default { ...@@ -55,8 +65,11 @@ export default {
}; };
}, },
computed: { computed: {
isCustomEmoji() {
return this.emoji !== this.defaultEmoji;
},
isDirty() { isDirty() {
return this.message.length || this.emoji.length; return Boolean(this.message.length || this.isCustomEmoji);
}, },
}, },
mounted() { mounted() {
...@@ -80,7 +93,7 @@ export default { ...@@ -80,7 +93,7 @@ export default {
this.emojiTag = Emoji.glEmojiTag(this.emoji); this.emojiTag = Emoji.glEmojiTag(this.emoji);
} }
this.noEmoji = this.emoji === ''; this.noEmoji = this.emoji === '';
this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon'); this.defaultEmojiTag = Emoji.glEmojiTag(this.defaultEmoji);
this.emojiMenu = new EmojiMenuInModal( this.emojiMenu = new EmojiMenuInModal(
Emoji, Emoji,
...@@ -89,6 +102,7 @@ export default { ...@@ -89,6 +102,7 @@ export default {
this.setEmoji, this.setEmoji,
this.$refs.userStatusForm, this.$refs.userStatusForm,
); );
this.setDefaultEmoji();
}) })
.catch(() => createFlash(__('Failed to load emoji list.'))); .catch(() => createFlash(__('Failed to load emoji list.')));
}, },
...@@ -107,7 +121,7 @@ export default { ...@@ -107,7 +121,7 @@ export default {
}, },
setDefaultEmoji() { setDefaultEmoji() {
const { emojiTag } = this; const { emojiTag } = this;
const hasStatusMessage = this.message; const hasStatusMessage = Boolean(this.message.length);
if (hasStatusMessage && emojiTag) { if (hasStatusMessage && emojiTag) {
return; return;
} }
...@@ -139,20 +153,26 @@ export default { ...@@ -139,20 +153,26 @@ export default {
this.hideEmojiMenu(); this.hideEmojiMenu();
}, },
removeStatus() { removeStatus() {
this.availability = false;
this.clearStatusInputs(); this.clearStatusInputs();
this.setStatus(); this.setStatus();
}, },
setStatus() { setStatus() {
const { emoji, message } = this; const { emoji, message, availability } = this;
Api.postUserStatus({ Api.postUserStatus({
emoji, emoji,
message, message,
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,
}) })
.then(this.onUpdateSuccess) .then(this.onUpdateSuccess)
.catch(this.onUpdateFail); .catch(this.onUpdateFail);
}, },
onUpdateSuccess() { onUpdateSuccess() {
this.$toast.show(s__('SetStatusModal|Status updated'), {
type: 'success',
position: 'top-center',
});
this.closeModal(); this.closeModal();
window.location.reload(); window.location.reload();
}, },
...@@ -188,7 +208,7 @@ export default { ...@@ -188,7 +208,7 @@ export default {
name="user[status][emoji]" name="user[status][emoji]"
/> />
<div ref="userStatusForm" class="form-group position-relative m-0"> <div ref="userStatusForm" class="form-group position-relative m-0">
<div class="input-group"> <div class="input-group gl-mb-5">
<span class="input-group-prepend"> <span class="input-group-prepend">
<button <button
ref="toggleEmojiMenuButton" ref="toggleEmojiMenuButton"
...@@ -236,6 +256,22 @@ export default { ...@@ -236,6 +256,22 @@ export default {
</button> </button>
</span> </span>
</div> </div>
<div v-if="canSetUserAvailability" class="form-group">
<div class="gl-display-flex">
<gl-form-checkbox
v-model="availability"
data-testid="user-availability-checkbox"
class="gl-mb-0"
>
<span class="gl-font-weight-bold">{{ s__('SetStatusModal|Busy') }}</span>
</gl-form-checkbox>
</div>
<div class="gl-display-flex">
<span class="gl-text-gray-600 gl-ml-5">
{{ s__('SetStatusModal|"Busy" will be shown next to your name') }}
</span>
</div>
</div>
</div> </div>
</div> </div>
</gl-modal> </gl-modal>
......
...@@ -158,6 +158,17 @@ module PageLayoutHelper ...@@ -158,6 +158,17 @@ module PageLayoutHelper
end end
end end
def user_status_properties(user)
default_properties = { current_emoji: '', current_message: '', can_set_user_availability: Feature.enabled?(:set_user_availability_status, user), default_emoji: UserStatus::DEFAULT_EMOJI }
return default_properties unless user&.status
default_properties.merge({
current_emoji: user.status.emoji.to_s,
current_message: user.status.message.to_s,
current_availability: user.status.availability.to_s
})
end
private private
def generic_canonical_url def generic_canonical_url
......
...@@ -37,4 +37,10 @@ module ProfilesHelper ...@@ -37,4 +37,10 @@ module ProfilesHelper
def user_status_set_to_busy?(status) def user_status_set_to_busy?(status)
status&.availability == availability_values[:busy] status&.availability == availability_values[:busy]
end end
def show_status_emoji?(status)
return false unless status
status.message.present? || status.emoji != UserStatus::DEFAULT_EMOJI
end
end end
...@@ -9,7 +9,8 @@ ...@@ -9,7 +9,8 @@
= current_user.to_reference = current_user.to_reference
- if current_user.status - if current_user.status
.user-status.d-flex.align-items-center.gl-mt-2.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } } .user-status.d-flex.align-items-center.gl-mt-2.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
%span.user-status-emoji.d-flex.align-items-center - if show_status_emoji?(current_user.status)
.user-status-emoji.d-flex.align-items-center
= emoji_icon current_user.status.emoji = emoji_icon current_user.status.emoji
%span.user-status-message.str-truncated %span.user-status-message.str-truncated
= current_user.status.message_html.html_safe = current_user.status.message_html.html_safe
......
- has_impersonation_link = header_link?(:admin_impersonation) - has_impersonation_link = header_link?(:admin_impersonation)
- user_status_data = user_status_properties(current_user)
%header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } } %header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
...@@ -103,4 +104,4 @@ ...@@ -103,4 +104,4 @@
#whats-new-app{ data: { storage_key: whats_new_storage_key } } #whats-new-app{ data: { storage_key: whats_new_storage_key } }
- if can?(current_user, :update_user_status, current_user) - if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } } .js-set-status-modal-wrapper{ data: user_status_data }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
- page_title s_("Profiles|Edit Profile") - page_title s_("Profiles|Edit Profile")
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host - gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
- availability = availability_values
- custom_emoji = show_status_emoji?(@user.status)
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user gl-mt-3 js-quick-submit gl-show-field-errors' }, authenticity_token: true do |f| = bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user gl-mt-3 js-quick-submit gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@user) = form_errors(@user)
...@@ -48,9 +50,9 @@ ...@@ -48,9 +50,9 @@
- emoji_button = button_tag type: :button, - emoji_button = button_tag type: :button,
class: 'js-toggle-emoji-menu emoji-menu-toggle-button gl-button btn has-tooltip', class: 'js-toggle-emoji-menu emoji-menu-toggle-button gl-button btn has-tooltip',
title: s_("Profiles|Add status emoji") do title: s_("Profiles|Add status emoji") do
- if @user.status - if custom_emoji
= emoji_icon @user.status.emoji = emoji_icon @user.status.emoji
%span#js-no-emoji-placeholder.no-emoji-placeholder{ class: ('hidden' if @user.status) } %span#js-no-emoji-placeholder.no-emoji-placeholder{ class: ('hidden' if custom_emoji) }
= sprite_icon('slight-smile', css_class: 'award-control-icon-neutral') = sprite_icon('slight-smile', css_class: 'award-control-icon-neutral')
= sprite_icon('smiley', css_class: 'award-control-icon-positive') = sprite_icon('smiley', css_class: 'award-control-icon-positive')
= sprite_icon('smile', css_class: 'award-control-icon-super-positive') = sprite_icon('smile', css_class: 'award-control-icon-super-positive')
...@@ -68,6 +70,10 @@ ...@@ -68,6 +70,10 @@
prepend: emoji_button, prepend: emoji_button,
append: reset_message_button, append: reset_message_button,
placeholder: s_("Profiles|What's your status?") placeholder: s_("Profiles|What's your status?")
- if Feature.enabled?(:set_user_availability_status, @user)
.checkbox-icon-inline-wrapper
= status_form.check_box :availability, { data: { testid: "user-availability-checkbox" }, label: s_("Profiles|Busy"), wrapper_class: 'gl-mr-0 gl-font-weight-bold' }, availability["busy"], availability["not_set"]
.gl-text-gray-600.gl-ml-5= s_('Profiles|"Busy" will be shown next to your name')
- if Feature.enabled?(:user_time_settings) - if Feature.enabled?(:user_time_settings)
%hr %hr
.row.user-time-preferences .row.user-time-preferences
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
- if @user&.status && user_status_set_to_busy?(@user.status) - if @user&.status && user_status_set_to_busy?(@user.status)
%span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle= s_("UserProfile|(Busy)") %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle= s_("UserProfile|(Busy)")
- if @user.status - if show_status_emoji?(@user.status)
.cover-status .cover-status
= emoji_icon(@user.status.emoji) = emoji_icon(@user.status.emoji)
= markdown_field(@user.status, :message) = markdown_field(@user.status, :message)
......
---
name: set_user_availability_status
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46844
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/281073
milestone: '13.6'
type: development
group: group::optimize
default_enabled: false
...@@ -20577,6 +20577,9 @@ msgstr "" ...@@ -20577,6 +20577,9 @@ msgstr ""
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible." msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
msgstr "" msgstr ""
msgid "Profiles|\"Busy\" will be shown next to your name"
msgstr ""
msgid "Profiles|%{provider} Active" msgid "Profiles|%{provider} Active"
msgstr "" msgstr ""
...@@ -20607,6 +20610,9 @@ msgstr "" ...@@ -20607,6 +20610,9 @@ msgstr ""
msgid "Profiles|Bio" msgid "Profiles|Bio"
msgstr "" msgstr ""
msgid "Profiles|Busy"
msgstr ""
msgid "Profiles|Change username" msgid "Profiles|Change username"
msgstr "" msgstr ""
...@@ -24734,9 +24740,15 @@ msgstr "" ...@@ -24734,9 +24740,15 @@ msgstr ""
msgid "SetPasswordToCloneLink|set a password" msgid "SetPasswordToCloneLink|set a password"
msgstr "" msgstr ""
msgid "SetStatusModal|\"Busy\" will be shown next to your name"
msgstr ""
msgid "SetStatusModal|Add status emoji" msgid "SetStatusModal|Add status emoji"
msgstr "" msgstr ""
msgid "SetStatusModal|Busy"
msgstr ""
msgid "SetStatusModal|Clear status" msgid "SetStatusModal|Clear status"
msgstr "" msgstr ""
...@@ -24755,6 +24767,9 @@ msgstr "" ...@@ -24755,6 +24767,9 @@ msgstr ""
msgid "SetStatusModal|Sorry, we weren't able to set your status. Please try again later." msgid "SetStatusModal|Sorry, we weren't able to set your status. Please try again later."
msgstr "" msgstr ""
msgid "SetStatusModal|Status updated"
msgstr ""
msgid "SetStatusModal|What's your status?" msgid "SetStatusModal|What's your status?"
msgstr "" msgstr ""
......
...@@ -20,6 +20,10 @@ RSpec.describe 'User edit profile' do ...@@ -20,6 +20,10 @@ RSpec.describe 'User edit profile' do
wait_for_requests wait_for_requests
end end
def toggle_busy_status
find('[data-testid="user-availability-checkbox"]').set(true)
end
it 'changes user profile' do it 'changes user profile' do
fill_in 'user_skype', with: 'testskype' fill_in 'user_skype', with: 'testskype'
fill_in 'user_linkedin', with: 'testlinkedin' fill_in 'user_linkedin', with: 'testlinkedin'
...@@ -180,20 +184,51 @@ RSpec.describe 'User edit profile' do ...@@ -180,20 +184,51 @@ RSpec.describe 'User edit profile' do
expect(page).to have_emoji('speech_balloon') expect(page).to have_emoji('speech_balloon')
end end
end end
it 'sets the users status to busy' do
busy_status = find('[data-testid="user-availability-checkbox"]')
expect(busy_status.checked?).to eq(false)
toggle_busy_status
submit_settings
visit profile_path
expect(busy_status.checked?).to eq(true)
end
context 'with set_user_availability_status feature flag disabled' do
before do
stub_feature_flags(set_user_availability_status: false)
visit root_path(user)
end
it 'does not display the availability checkbox' do
expect(page).not_to have_css('[data-testid="user-availability-checkbox"]')
end
end
end end
context 'user menu' do context 'user menu' do
let(:issue) { create(:issue, project: project)} let(:issue) { create(:issue, project: project)}
let(:project) { create(:project) } let(:project) { create(:project) }
def open_user_status_modal def open_modal(button_text)
find('.header-user-dropdown-toggle').click find('.header-user-dropdown-toggle').click
page.within ".header-user" do page.within ".header-user" do
click_button 'Set status' click_button button_text
end end
end end
def open_user_status_modal
open_modal 'Set status'
end
def open_edit_status_modal
open_modal 'Edit status'
end
def set_user_status_in_modal def set_user_status_in_modal
page.within "#set-user-status-modal" do page.within "#set-user-status-modal" do
click_button 'Set status' click_button 'Set status'
...@@ -246,6 +281,19 @@ RSpec.describe 'User edit profile' do ...@@ -246,6 +281,19 @@ RSpec.describe 'User edit profile' do
end end
end end
it 'sets the users status to busy' do
open_user_status_modal
busy_status = find('[data-testid="user-availability-checkbox"]')
expect(busy_status.checked?).to eq(false)
toggle_busy_status
set_user_status_in_modal
open_edit_status_modal
expect(busy_status.checked?).to eq(true)
end
it 'opens the emoji modal again after closing it' do it 'opens the emoji modal again after closing it' do
open_user_status_modal open_user_status_modal
select_emoji('biohazard', true) select_emoji('biohazard', true)
...@@ -307,11 +355,7 @@ RSpec.describe 'User edit profile' do ...@@ -307,11 +355,7 @@ RSpec.describe 'User edit profile' do
expect(page).to have_content user_status.message expect(page).to have_content user_status.message
end end
find('.header-user-dropdown-toggle').click open_edit_status_modal
page.within ".header-user" do
click_button 'Edit status'
end
find('.js-clear-user-status-button').click find('.js-clear-user-status-button').click
set_user_status_in_modal set_user_status_in_modal
...@@ -333,11 +377,7 @@ RSpec.describe 'User edit profile' do ...@@ -333,11 +377,7 @@ RSpec.describe 'User edit profile' do
expect(page).to have_content user_status.message expect(page).to have_content user_status.message
end end
find('.header-user-dropdown-toggle').click open_edit_status_modal
page.within ".header-user" do
click_button 'Edit status'
end
page.within "#set-user-status-modal" do page.within "#set-user-status-modal" do
click_button 'Remove status' click_button 'Remove status'
...@@ -357,6 +397,19 @@ RSpec.describe 'User edit profile' do ...@@ -357,6 +397,19 @@ RSpec.describe 'User edit profile' do
expect(page).to have_emoji('speech_balloon') expect(page).to have_emoji('speech_balloon')
end end
end end
context 'with set_user_availability_status feature flag disabled' do
before do
stub_feature_flags(set_user_availability_status: false)
visit root_path(user)
end
it 'does not display the availability checkbox' do
open_user_status_modal
expect(page).not_to have_css('[data-testid="user-availability-checkbox"]')
end
end
end end
context 'User time preferences', :js do context 'User time preferences', :js do
......
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
import { initEmojiMock } from 'helpers/emoji';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import SetStatusModalWrapper, {
AVAILABILITY_STATUS,
} from '~/set_status_modal/set_status_modal_wrapper.vue';
jest.mock('~/api');
jest.mock('~/flash');
describe('SetStatusModalWrapper', () => {
let wrapper;
let mockEmoji;
const $toast = {
show: jest.fn(),
};
const defaultEmoji = 'speech_balloon';
const defaultMessage = "They're comin' in too fast!";
const defaultProps = {
currentEmoji: defaultEmoji,
currentMessage: defaultMessage,
defaultEmoji,
canSetUserAvailability: true,
};
const createComponent = (props = {}) => {
return shallowMount(SetStatusModalWrapper, {
propsData: {
...defaultProps,
...props,
},
mocks: {
$toast,
},
});
};
const findModal = () => wrapper.find(GlModal);
const findFormField = field => wrapper.find(`[name="user[status][${field}]"]`);
const findClearStatusButton = () => wrapper.find('.js-clear-user-status-button');
const findNoEmojiPlaceholder = () => wrapper.find('.js-no-emoji-placeholder');
const findToggleEmojiButton = () => wrapper.find('.js-toggle-emoji-menu');
const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox);
const initModal = ({ mockOnUpdateSuccess = true, mockOnUpdateFailure = true } = {}) => {
const modal = findModal();
// mock internal emoji methods
wrapper.vm.showEmojiMenu = jest.fn();
wrapper.vm.hideEmojiMenu = jest.fn();
if (mockOnUpdateSuccess) wrapper.vm.onUpdateSuccess = jest.fn();
if (mockOnUpdateFailure) wrapper.vm.onUpdateFail = jest.fn();
modal.vm.$emit('shown');
return wrapper.vm.$nextTick();
};
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent();
return initModal();
});
afterEach(() => {
wrapper.destroy();
mockEmoji.restore();
});
describe('with minimum props', () => {
it('sets the hidden status emoji field', () => {
const field = findFormField('emoji');
expect(field.exists()).toBe(true);
expect(field.element.value).toBe(defaultEmoji);
});
it('sets the message field', () => {
const field = findFormField('message');
expect(field.exists()).toBe(true);
expect(field.element.value).toBe(defaultMessage);
});
it('sets the availability field to false', () => {
const field = findAvailabilityCheckbox();
expect(field.exists()).toBe(true);
expect(field.element.checked).toBeUndefined();
});
it('has a clear status button', () => {
expect(findClearStatusButton().isVisible()).toBe(true);
});
it('clicking the toggle emoji button displays the emoji list', () => {
expect(wrapper.vm.showEmojiMenu).not.toHaveBeenCalled();
findToggleEmojiButton().trigger('click');
expect(wrapper.vm.showEmojiMenu).toHaveBeenCalled();
});
});
describe('with no currentMessage set', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ currentMessage: '' });
return initModal();
});
it('does not set the message field', () => {
expect(findFormField('message').element.value).toBe('');
});
it('hides the clear status button', () => {
expect(findClearStatusButton().isVisible()).toBe(false);
});
it('shows the placeholder emoji', () => {
expect(findNoEmojiPlaceholder().isVisible()).toBe(true);
});
});
describe('with no currentEmoji set', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ currentEmoji: '' });
return initModal();
});
it('does not set the hidden status emoji field', () => {
expect(findFormField('emoji').element.value).toBe('');
});
it('hides the placeholder emoji', () => {
expect(findNoEmojiPlaceholder().isVisible()).toBe(false);
});
describe('with no currentMessage set', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
return initModal();
});
it('shows the placeholder emoji', () => {
expect(findNoEmojiPlaceholder().isVisible()).toBe(true);
});
});
});
describe('update status', () => {
describe('succeeds', () => {
beforeEach(() => {
jest.spyOn(Api, 'postUserStatus').mockResolvedValue();
});
it('clicking "removeStatus" clears the emoji and message fields', async () => {
findModal().vm.$emit('cancel');
await wrapper.vm.$nextTick();
expect(findFormField('message').element.value).toBe('');
expect(findFormField('emoji').element.value).toBe('');
});
it('clicking "setStatus" submits the user status', async () => {
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
// set the availability status
findAvailabilityCheckbox().vm.$emit('input', true);
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
const commonParams = { emoji: defaultEmoji, message: defaultMessage };
expect(Api.postUserStatus).toHaveBeenCalledTimes(2);
expect(Api.postUserStatus).toHaveBeenNthCalledWith(1, {
availability: AVAILABILITY_STATUS.NOT_SET,
...commonParams,
});
expect(Api.postUserStatus).toHaveBeenNthCalledWith(2, {
availability: AVAILABILITY_STATUS.BUSY,
...commonParams,
});
});
it('calls the "onUpdateSuccess" handler', async () => {
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
expect(wrapper.vm.onUpdateSuccess).toHaveBeenCalled();
});
});
describe('success message', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
jest.spyOn(Api, 'postUserStatus').mockResolvedValue();
return initModal({ mockOnUpdateSuccess: false });
});
it('displays a toast success message', async () => {
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
expect($toast.show).toHaveBeenCalledWith('Status updated', {
position: 'top-center',
type: 'success',
});
});
});
describe('with errors', () => {
beforeEach(() => {
jest.spyOn(Api, 'postUserStatus').mockRejectedValue();
});
it('calls the "onUpdateFail" handler', async () => {
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
expect(wrapper.vm.onUpdateFail).toHaveBeenCalled();
});
});
describe('error message', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
jest.spyOn(Api, 'postUserStatus').mockRejectedValue();
return initModal({ mockOnUpdateFailure: false });
});
it('flashes an error message', async () => {
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
expect(createFlash).toHaveBeenCalledWith(
"Sorry, we weren't able to set your status. Please try again later.",
);
});
});
});
describe('with canSetUserAvailability=false', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ canSetUserAvailability: false });
return initModal();
});
it('hides the set availability checkbox', () => {
expect(findAvailabilityCheckbox().exists()).toBe(false);
});
});
});
...@@ -221,4 +221,42 @@ RSpec.describe PageLayoutHelper do ...@@ -221,4 +221,42 @@ RSpec.describe PageLayoutHelper do
end end
end end
end end
describe '#user_status_properties' do
using RSpec::Parameterized::TableSyntax
let(:user) { build(:user) }
availability_types = Types::AvailabilityEnum.enum
where(:message, :emoji, :availability) do
"Some message" | UserStatus::DEFAULT_EMOJI | availability_types[:busy]
"Some message" | UserStatus::DEFAULT_EMOJI | availability_types[:not_set]
"Some message" | "basketball" | availability_types[:busy]
"Some message" | "basketball" | availability_types[:not_set]
"Some message" | "" | availability_types[:busy]
"Some message" | "" | availability_types[:not_set]
"" | UserStatus::DEFAULT_EMOJI | availability_types[:busy]
"" | UserStatus::DEFAULT_EMOJI | availability_types[:not_set]
"" | "basketball" | availability_types[:busy]
"" | "basketball" | availability_types[:not_set]
"" | "" | availability_types[:busy]
"" | "" | availability_types[:not_set]
end
with_them do
it "sets the default user status fields" do
user.status = UserStatus.new(message: message, emoji: emoji, availability: availability)
result = {
can_set_user_availability: true,
current_availability: availability,
current_emoji: emoji,
current_message: message,
default_emoji: UserStatus::DEFAULT_EMOJI
}
expect(helper.user_status_properties(user)).to eq(result)
end
end
end
end end
...@@ -95,6 +95,23 @@ RSpec.describe ProfilesHelper do ...@@ -95,6 +95,23 @@ RSpec.describe ProfilesHelper do
end end
end end
describe "#show_status_emoji?" do
using RSpec::Parameterized::TableSyntax
where(:message, :emoji, :result) do
"Some message" | UserStatus::DEFAULT_EMOJI | true
"Some message" | "" | true
"" | "basketball" | true
"" | "basketball" | true
"" | UserStatus::DEFAULT_EMOJI | false
"" | UserStatus::DEFAULT_EMOJI | false
end
with_them do
it { expect(helper.show_status_emoji?(OpenStruct.new(message: message, emoji: emoji))).to eq(result) }
end
end
def stub_cas_omniauth_provider def stub_cas_omniauth_provider
provider = OpenStruct.new( provider = OpenStruct.new(
'name' => 'cas3', 'name' => 'cas3',
......
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