Commit d1eae71c authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '263450-fe-set-user-availability-add-busy-checkbox' into 'master'

[FE] Set user availability - Add busy checkbox

See merge request gitlab-org/gitlab!46844
parents 82054e57 95f4f26a
...@@ -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,8 +9,9 @@ ...@@ -9,8 +9,9 @@
= 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)
= emoji_icon current_user.status.emoji .user-status-emoji.d-flex.align-items-center
= 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
%li.divider %li.divider
......
- 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