import { GlSkeletonLoader, GlIcon } from '@gitlab/ui';
import mrDiffCommentFixture from 'test_fixtures/merge_requests/diff_comment.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import {
I18N_USER_BLOCKED,
I18N_USER_LEARN,
I18N_USER_FOLLOW,
I18N_ERROR_FOLLOW,
I18N_USER_UNFOLLOW,
I18N_ERROR_UNFOLLOW,
} from '~/vue_shared/components/user_popover/constants';
import axios from '~/lib/utils/axios_utils';
import { createAlert } from '~/alert';
import { followUser, unfollowUser } from '~/api/user_api';
import { mockTracking } from 'helpers/tracking_helper';
jest.mock('~/alert');
jest.mock('~/api/user_api', () => ({
followUser: jest.fn(),
unfollowUser: jest.fn(),
}));
const DEFAULT_PROPS = {
user: {
id: 1,
username: 'root',
name: 'Administrator',
location: 'Vienna',
localTime: '2:30 PM',
bot: false,
bio: null,
workInformation: null,
status: null,
pronouns: 'they/them',
isFollowed: false,
loaded: true,
},
};
describe('User Popover Component', () => {
let wrapper;
beforeEach(() => {
setHTMLFixture(mrDiffCommentFixture);
gon.features = {};
});
afterEach(() => {
resetHTMLFixture();
});
const findUserStatus = () => wrapper.findByTestId('user-popover-status');
const findTarget = () => document.querySelector('.js-user-link');
const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link');
const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time');
const findToggleFollowButton = () => wrapper.findByTestId('toggle-follow-button');
const itTracksToggleFollowButtonClick = (expectedLabel) => {
it('tracks click', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
await findToggleFollowButton().trigger('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: expectedLabel,
});
});
};
const createWrapper = (props = {}) => {
wrapper = mountExtended(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: findTarget(),
...props,
},
});
};
describe('when user is loading', () => {
it('displays skeleton loader', () => {
createWrapper({
user: {
name: null,
username: null,
location: null,
bio: null,
workInformation: null,
status: null,
loaded: false,
},
});
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
});
describe('basic data', () => {
it('should show basic fields', () => {
createWrapper();
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name);
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username);
});
it('shows icon for location', () => {
createWrapper();
const iconEl = wrapper.findComponent(GlIcon);
expect(iconEl.props('name')).toEqual('location');
});
it("should not show a link to bot's documentation", () => {
createWrapper();
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.exists()).toBe(false);
});
});
describe('job data', () => {
const findWorkInformation = () => wrapper.findComponent({ ref: 'workInformation' });
const findBio = () => wrapper.findComponent({ ref: 'bio' });
const bio = 'My super interesting bio';
it('should show only bio if work information is not available', () => {
const user = { ...DEFAULT_PROPS.user, bio };
createWrapper({ user });
expect(findBio().text()).toBe('My super interesting bio');
expect(findWorkInformation().exists()).toBe(false);
});
it('should show work information when it is available', () => {
const user = {
...DEFAULT_PROPS.user,
workInformation: 'Frontend Engineer at GitLab',
};
createWrapper({ user });
expect(findWorkInformation().text()).toBe('Frontend Engineer at GitLab');
});
it('should display bio and work information in separate lines', () => {
const user = {
...DEFAULT_PROPS.user,
bio,
workInformation: 'Frontend Engineer at GitLab',
};
createWrapper({ user });
expect(findBio().text()).toBe('My super interesting bio');
expect(findWorkInformation().text()).toBe('Frontend Engineer at GitLab');
});
it('should encode special characters in bio', () => {
const user = {
...DEFAULT_PROPS.user,
bio: 'I like CSS',
};
createWrapper({ user });
expect(findBio().html()).toContain('I like <b>CSS</b>');
});
it('shows icon for bio', () => {
const user = {
...DEFAULT_PROPS.user,
bio: 'My super interesting bio',
};
createWrapper({ user });
expect(
wrapper.findAllComponents(GlIcon).filter((icon) => icon.props('name') === 'profile').length,
).toEqual(1);
});
it('shows icon for work information', () => {
const user = {
...DEFAULT_PROPS.user,
workInformation: 'GitLab',
};
createWrapper({ user });
expect(
wrapper.findAllComponents(GlIcon).filter((icon) => icon.props('name') === 'work').length,
).toEqual(1);
});
});
describe('local time', () => {
it('should show local time when it is available', () => {
createWrapper();
expect(findUserLocalTime().exists()).toBe(true);
});
it('should not show local time when it is not available', () => {
const user = {
...DEFAULT_PROPS.user,
localTime: null,
};
createWrapper({ user });
expect(findUserLocalTime().exists()).toBe(false);
});
});
describe('status data', () => {
it('should show only message', () => {
const user = { ...DEFAULT_PROPS.user, status: { message_html: 'Hello World' } };
createWrapper({ user });
expect(findUserStatus().exists()).toBe(true);
expect(wrapper.text()).toContain('Hello World');
});
it('should show message and emoji', () => {
const user = {
...DEFAULT_PROPS.user,
status: { emoji: 'basketball_player', message_html: 'Hello World' },
};
createWrapper({ user });
expect(findUserStatus().exists()).toBe(true);
expect(wrapper.text()).toContain('Hello World');
expect(wrapper.html()).toContain(' {
const user = {
...DEFAULT_PROPS.user,
status: { emoji: 'basketball_player' },
};
createWrapper({ user });
expect(findUserStatus().exists()).toBe(true);
expect(wrapper.html()).toContain(' {
const user = { ...DEFAULT_PROPS.user, status: null };
createWrapper({ user });
expect(findUserStatus().exists()).toBe(false);
});
it('hides the div when status is empty', () => {
const user = { ...DEFAULT_PROPS.user, status: { emoji: '', message_html: '' } };
createWrapper({ user });
expect(findUserStatus().exists()).toBe(false);
});
it('should show the busy status if user set to busy', () => {
const user = {
...DEFAULT_PROPS.user,
status: { availability: AVAILABILITY_STATUS.BUSY },
};
createWrapper({ user });
expect(wrapper.findByText('Busy').exists()).toBe(true);
});
it('should hide the busy status for any other status', () => {
const user = {
...DEFAULT_PROPS.user,
status: { availability: AVAILABILITY_STATUS.NOT_SET },
};
createWrapper({ user });
expect(wrapper.findByText('Busy').exists()).toBe(false);
});
it('shows pronouns when user has them set', () => {
createWrapper();
expect(wrapper.findByText('(they/them)').exists()).toBe(true);
});
describe.each`
pronouns
${undefined}
${null}
${''}
${' '}
`('when pronouns are set to $pronouns', ({ pronouns }) => {
it('does not render pronouns', () => {
const user = {
...DEFAULT_PROPS.user,
pronouns,
};
createWrapper({ user });
expect(wrapper.findByTestId('user-popover-pronouns').exists()).toBe(false);
});
});
});
describe('bot user', () => {
const SECURITY_BOT_USER = {
...DEFAULT_PROPS.user,
name: 'GitLab Security Bot',
username: 'GitLab-Security-Bot',
websiteUrl: '/security/bot/docs',
bot: true,
};
it("shows a link to the bot's documentation", () => {
createWrapper({ user: SECURITY_BOT_USER });
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.exists()).toBe(true);
expect(securityBotDocsLink.attributes('href')).toBe(SECURITY_BOT_USER.websiteUrl);
expect(securityBotDocsLink.text()).toBe(
sprintf(I18N_USER_LEARN, { name: SECURITY_BOT_USER.name }),
);
});
it("does not show a link to the bot's documentation if there is no website_url", () => {
createWrapper({ user: { ...SECURITY_BOT_USER, websiteUrl: null } });
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.exists()).toBe(false);
});
it("doesn't escape user's name", () => {
const name = '%<>\';"';
createWrapper({ user: { ...SECURITY_BOT_USER, name } });
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.text()).toBe(sprintf(I18N_USER_LEARN, { name }, false));
});
it('does not display local time', () => {
createWrapper({ user: SECURITY_BOT_USER });
expect(findUserLocalTime().exists()).toBe(false);
});
});
describe("when current user doesn't follow the user", () => {
beforeEach(() => createWrapper());
it('renders the Follow button with the correct variant', () => {
expect(findToggleFollowButton().text()).toBe(I18N_USER_FOLLOW);
expect(findToggleFollowButton().props('variant')).toBe('confirm');
});
describe('when clicking', () => {
it('follows the user', async () => {
followUser.mockResolvedValue({});
await findToggleFollowButton().trigger('click');
expect(findToggleFollowButton().props('loading')).toBe(true);
await axios.waitForAll();
expect(wrapper.emitted().follow.length).toBe(1);
expect(wrapper.emitted().unfollow).toBeUndefined();
});
itTracksToggleFollowButtonClick('follow_from_user_popover');
describe('when an error occurs', () => {
describe('api send error message', () => {
const mockedMessage = sprintf(I18N_ERROR_UNFOLLOW, { limit: 300 });
const apiResponse = { response: { data: { message: mockedMessage } } };
beforeEach(() => {
followUser.mockRejectedValue(apiResponse);
findToggleFollowButton().trigger('click');
});
it('show an error message from api response', async () => {
await axios.waitForAll();
expect(createAlert).toHaveBeenCalledWith({
message: mockedMessage,
error: apiResponse,
captureError: true,
});
});
});
describe('api did not send error message', () => {
beforeEach(() => {
followUser.mockRejectedValue({});
findToggleFollowButton().trigger('click');
});
it('shows an error message', async () => {
await axios.waitForAll();
expect(createAlert).toHaveBeenCalledWith({
message: I18N_ERROR_FOLLOW,
error: {},
captureError: true,
});
});
it('emits no events', async () => {
await axios.waitForAll();
expect(wrapper.emitted().follow).toBeUndefined();
expect(wrapper.emitted().unfollow).toBeUndefined();
});
});
});
});
});
describe('when current user follows the user', () => {
beforeEach(() => createWrapper({ user: { ...DEFAULT_PROPS.user, isFollowed: true } }));
it('renders the Unfollow button with the correct variant', () => {
expect(findToggleFollowButton().text()).toBe(I18N_USER_UNFOLLOW);
expect(findToggleFollowButton().props('variant')).toBe('default');
});
describe('when clicking', () => {
it('unfollows the user', async () => {
unfollowUser.mockResolvedValue({});
findToggleFollowButton().trigger('click');
await axios.waitForAll();
expect(wrapper.emitted().follow).toBe(undefined);
expect(wrapper.emitted().unfollow.length).toBe(1);
});
itTracksToggleFollowButtonClick('unfollow_from_user_popover');
describe('when an error occurs', () => {
beforeEach(async () => {
unfollowUser.mockRejectedValue({});
findToggleFollowButton().trigger('click');
await axios.waitForAll();
});
it('shows an error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: I18N_ERROR_UNFOLLOW,
error: {},
captureError: true,
});
});
it('emits no events', () => {
expect(wrapper.emitted().follow).toBeUndefined();
expect(wrapper.emitted().unfollow).toBeUndefined();
});
});
});
});
describe('when the current user is the user', () => {
beforeEach(() => {
gon.current_username = DEFAULT_PROPS.user.username;
createWrapper();
});
it("doesn't render the toggle follow button", () => {
expect(findToggleFollowButton().exists()).toBe(false);
});
});
describe('when the user is blocked', () => {
const bio = 'My super interesting bio';
const status = 'My status';
beforeEach(() =>
createWrapper({
user: { ...DEFAULT_PROPS.user, state: 'blocked', bio, status: { message_html: status } },
}),
);
it('renders warning', () => {
expect(wrapper.text()).toContain(I18N_USER_BLOCKED);
});
it("doesn't show other information", () => {
expect(wrapper.text()).not.toContain(bio);
expect(wrapper.text()).not.toContain(status);
});
});
describe('when API does not support `isFollowed`', () => {
beforeEach(() => {
const user = {
...DEFAULT_PROPS.user,
isFollowed: undefined,
};
createWrapper({ user });
});
it('does not render the toggle follow button', () => {
expect(findToggleFollowButton().exists()).toBe(false);
});
});
});