diff options
Diffstat (limited to 'spec/frontend/profile/components')
6 files changed, 388 insertions, 13 deletions
diff --git a/spec/frontend/profile/components/activity_calendar_spec.js b/spec/frontend/profile/components/activity_calendar_spec.js new file mode 100644 index 00000000000..fb9dc7b22f7 --- /dev/null +++ b/spec/frontend/profile/components/activity_calendar_spec.js @@ -0,0 +1,120 @@ +import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import * as GitLabUIUtils from '@gitlab/ui/dist/utils'; + +import ActivityCalendar from '~/profile/components/activity_calendar.vue'; +import AjaxCache from '~/lib/utils/ajax_cache'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { useFakeDate } from 'helpers/fake_date'; +import { userCalendarResponse } from '../mock_data'; + +jest.mock('~/lib/utils/ajax_cache'); +jest.mock('@gitlab/ui/dist/utils'); + +describe('ActivityCalendar', () => { + // Feb 21st, 2023 + useFakeDate(2023, 1, 21); + + let wrapper; + + const defaultProvide = { + userCalendarPath: '/users/root/calendar.json', + utcOffset: '0', + }; + + const createComponent = () => { + wrapper = mountExtended(ActivityCalendar, { provide: defaultProvide }); + }; + + const mockSuccessfulApiRequest = () => + AjaxCache.retrieve.mockResolvedValueOnce(userCalendarResponse); + const mockUnsuccessfulApiRequest = () => AjaxCache.retrieve.mockRejectedValueOnce(); + + const findCalendar = () => wrapper.findByTestId('contrib-calendar'); + + describe('when API request is loading', () => { + beforeEach(() => { + AjaxCache.retrieve.mockReturnValueOnce(new Promise(() => {})); + }); + + it('renders loading icon', () => { + createComponent(); + + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + }); + }); + + describe('when API request is successful', () => { + beforeEach(() => { + mockSuccessfulApiRequest(); + }); + + it('renders the calendar', async () => { + createComponent(); + + await waitForPromises(); + + expect(findCalendar().exists()).toBe(true); + expect(wrapper.findByText(ActivityCalendar.i18n.calendarHint).exists()).toBe(true); + }); + + describe('when window is resized', () => { + it('re-renders the calendar', async () => { + createComponent(); + + await waitForPromises(); + + mockSuccessfulApiRequest(); + window.innerWidth = 1200; + window.dispatchEvent(new Event('resize')); + + await waitForPromises(); + + expect(findCalendar().exists()).toBe(true); + expect(AjaxCache.retrieve).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe('when API request is not successful', () => { + beforeEach(() => { + mockUnsuccessfulApiRequest(); + }); + + it('renders error', async () => { + createComponent(); + + await waitForPromises(); + + expect(wrapper.findComponent(GlAlert).exists()).toBe(true); + }); + + describe('when retry button is clicked', () => { + it('retries API request', async () => { + createComponent(); + + await waitForPromises(); + + mockSuccessfulApiRequest(); + + await wrapper.findByRole('button', { name: ActivityCalendar.i18n.retry }).trigger('click'); + + await waitForPromises(); + + expect(findCalendar().exists()).toBe(true); + }); + }); + }); + + describe('when screen is extra small', () => { + beforeEach(() => { + GitLabUIUtils.GlBreakpointInstance.getBreakpointSize.mockReturnValueOnce('xs'); + }); + + it('does not render the calendar', () => { + createComponent(); + + expect(findCalendar().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/profile/components/followers_tab_spec.js b/spec/frontend/profile/components/followers_tab_spec.js index 4af428c4e0c..9cc5bdea9be 100644 --- a/spec/frontend/profile/components/followers_tab_spec.js +++ b/spec/frontend/profile/components/followers_tab_spec.js @@ -1,4 +1,4 @@ -import { GlTab } from '@gitlab/ui'; +import { GlBadge, GlTab } from '@gitlab/ui'; import { s__ } from '~/locale'; import FollowersTab from '~/profile/components/followers_tab.vue'; @@ -8,12 +8,25 @@ describe('FollowersTab', () => { let wrapper; const createComponent = () => { - wrapper = shallowMountExtended(FollowersTab); + wrapper = shallowMountExtended(FollowersTab, { + provide: { + followers: 2, + }, + }); }; - it('renders `GlTab` and sets `title` prop', () => { + it('renders `GlTab` and sets title', () => { createComponent(); - expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Followers')); + expect(wrapper.findComponent(GlTab).element.textContent).toContain( + s__('UserProfile|Followers'), + ); + }); + + it('renders `GlBadge`, sets size and content', () => { + createComponent(); + + expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm'); + expect(wrapper.findComponent(GlBadge).element.textContent).toBe('2'); }); }); diff --git a/spec/frontend/profile/components/following_tab_spec.js b/spec/frontend/profile/components/following_tab_spec.js index 75123274ccb..c9d56360c3e 100644 --- a/spec/frontend/profile/components/following_tab_spec.js +++ b/spec/frontend/profile/components/following_tab_spec.js @@ -1,4 +1,4 @@ -import { GlTab } from '@gitlab/ui'; +import { GlBadge, GlTab } from '@gitlab/ui'; import { s__ } from '~/locale'; import FollowingTab from '~/profile/components/following_tab.vue'; @@ -8,12 +8,25 @@ describe('FollowingTab', () => { let wrapper; const createComponent = () => { - wrapper = shallowMountExtended(FollowingTab); + wrapper = shallowMountExtended(FollowingTab, { + provide: { + followees: 3, + }, + }); }; - it('renders `GlTab` and sets `title` prop', () => { + it('renders `GlTab` and sets title', () => { createComponent(); - expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Following')); + expect(wrapper.findComponent(GlTab).element.textContent).toContain( + s__('UserProfile|Following'), + ); + }); + + it('renders `GlBadge`, sets size and content', () => { + createComponent(); + + expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm'); + expect(wrapper.findComponent(GlBadge).element.textContent).toBe('3'); }); }); diff --git a/spec/frontend/profile/components/overview_tab_spec.js b/spec/frontend/profile/components/overview_tab_spec.js index eb27515bca3..aeab24cb730 100644 --- a/spec/frontend/profile/components/overview_tab_spec.js +++ b/spec/frontend/profile/components/overview_tab_spec.js @@ -1,14 +1,25 @@ -import { GlTab } from '@gitlab/ui'; +import { GlLoadingIcon, GlTab, GlLink } from '@gitlab/ui'; +import projects from 'test_fixtures/api/users/projects/get.json'; import { s__ } from '~/locale'; import OverviewTab from '~/profile/components/overview_tab.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ActivityCalendar from '~/profile/components/activity_calendar.vue'; +import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; describe('OverviewTab', () => { let wrapper; - const createComponent = () => { - wrapper = shallowMountExtended(OverviewTab); + const defaultPropsData = { + personalProjects: convertObjectPropsToCamelCase(projects, { deep: true }), + personalProjectsLoading: false, + }; + + const createComponent = ({ propsData = {} } = {}) => { + wrapper = shallowMountExtended(OverviewTab, { + propsData: { ...defaultPropsData, ...propsData }, + }); }; it('renders `GlTab` and sets `title` prop', () => { @@ -16,4 +27,47 @@ describe('OverviewTab', () => { expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Overview')); }); + + it('renders `ActivityCalendar` component', () => { + createComponent(); + + expect(wrapper.findComponent(ActivityCalendar).exists()).toBe(true); + }); + + it('renders personal projects section heading and `View all` link', () => { + createComponent(); + + expect( + wrapper.findByRole('heading', { name: OverviewTab.i18n.personalProjects }).exists(), + ).toBe(true); + expect(wrapper.findComponent(GlLink).text()).toBe(OverviewTab.i18n.viewAll); + }); + + describe('when personal projects are loading', () => { + it('renders loading icon', () => { + createComponent({ + propsData: { + personalProjects: [], + personalProjectsLoading: true, + }, + }); + + expect( + wrapper.findByTestId('personal-projects-section').findComponent(GlLoadingIcon).exists(), + ).toBe(true); + }); + }); + + describe('when projects are done loading', () => { + it('renders `ProjectsList` component and passes `projects` prop', () => { + createComponent(); + + expect( + wrapper + .findByTestId('personal-projects-section') + .findComponent(ProjectsList) + .props('projects'), + ).toMatchObject(defaultPropsData.personalProjects); + }); + }); }); diff --git a/spec/frontend/profile/components/profile_tabs_spec.js b/spec/frontend/profile/components/profile_tabs_spec.js index 11ab372f1dd..80a1ff422ab 100644 --- a/spec/frontend/profile/components/profile_tabs_spec.js +++ b/spec/frontend/profile/components/profile_tabs_spec.js @@ -1,6 +1,9 @@ +import projects from 'test_fixtures/api/users/projects/get.json'; import ProfileTabs from '~/profile/components/profile_tabs.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; - +import { createAlert } from '~/alert'; +import { getUserProjects } from '~/rest_api'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import OverviewTab from '~/profile/components/overview_tab.vue'; import ActivityTab from '~/profile/components/activity_tab.vue'; import GroupsTab from '~/profile/components/groups_tab.vue'; @@ -10,12 +13,20 @@ import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue'; import SnippetsTab from '~/profile/components/snippets_tab.vue'; import FollowersTab from '~/profile/components/followers_tab.vue'; import FollowingTab from '~/profile/components/following_tab.vue'; +import waitForPromises from 'helpers/wait_for_promises'; + +jest.mock('~/alert'); +jest.mock('~/rest_api'); describe('ProfileTabs', () => { let wrapper; const createComponent = () => { - wrapper = shallowMountExtended(ProfileTabs); + wrapper = shallowMountExtended(ProfileTabs, { + provide: { + userId: '1', + }, + }); }; it.each([ @@ -33,4 +44,46 @@ describe('ProfileTabs', () => { expect(wrapper.findComponent(tab).exists()).toBe(true); }); + + describe('when personal projects API request is loading', () => { + beforeEach(() => { + getUserProjects.mockReturnValueOnce(new Promise(() => {})); + createComponent(); + }); + + it('passes correct props to `OverviewTab` component', () => { + expect(wrapper.findComponent(OverviewTab).props()).toEqual({ + personalProjects: [], + personalProjectsLoading: true, + }); + }); + }); + + describe('when personal projects API request is successful', () => { + beforeEach(async () => { + getUserProjects.mockResolvedValueOnce({ data: projects }); + createComponent(); + await waitForPromises(); + }); + + it('passes correct props to `OverviewTab` component', () => { + expect(wrapper.findComponent(OverviewTab).props()).toMatchObject({ + personalProjects: convertObjectPropsToCamelCase(projects, { deep: true }), + personalProjectsLoading: false, + }); + }); + }); + + describe('when personal projects API request is not successful', () => { + beforeEach(() => { + getUserProjects.mockRejectedValueOnce(); + createComponent(); + }); + + it('calls `createAlert`', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: ProfileTabs.i18n.personalProjectsErrorMessage, + }); + }); + }); }); diff --git a/spec/frontend/profile/components/user_achievements_spec.js b/spec/frontend/profile/components/user_achievements_spec.js new file mode 100644 index 00000000000..ff6f323621a --- /dev/null +++ b/spec/frontend/profile/components/user_achievements_spec.js @@ -0,0 +1,122 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import getUserAchievementsEmptyResponse from 'test_fixtures/graphql/get_user_achievements_empty_response.json'; +import getUserAchievementsLongResponse from 'test_fixtures/graphql/get_user_achievements_long_response.json'; +import getUserAchievementsResponse from 'test_fixtures/graphql/get_user_achievements_with_avatar_and_description_response.json'; +import getUserAchievementsPrivateGroupResponse from 'test_fixtures/graphql/get_user_achievements_from_private_group.json'; +import getUserAchievementsNoAvatarResponse from 'test_fixtures/graphql/get_user_achievements_without_avatar_or_description_response.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import UserAchievements from '~/profile/components/user_achievements.vue'; +import getUserAchievements from '~/profile/components//graphql/get_user_achievements.query.graphql'; +import { getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; + +const USER_ID = 123; +const ROOT_URL = 'https://gitlab.com/'; +const PLACEHOLDER_URL = 'https://gitlab.com/assets/gitlab_logo.png'; +const userAchievement1 = getUserAchievementsResponse.data.user.userAchievements.nodes[0]; + +Vue.use(VueApollo); + +describe('UserAchievements', () => { + let wrapper; + + const getUserAchievementsQueryHandler = jest.fn().mockResolvedValue(getUserAchievementsResponse); + const achievement = () => wrapper.findByTestId('user-achievement'); + + const createComponent = ({ queryHandler = getUserAchievementsQueryHandler } = {}) => { + const fakeApollo = createMockApollo([[getUserAchievements, queryHandler]]); + + wrapper = mountExtended(UserAchievements, { + apolloProvider: fakeApollo, + provide: { + rootUrl: ROOT_URL, + userId: USER_ID, + }, + }); + }; + + it('renders no achievements on reject', async () => { + createComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') }); + + await waitForPromises(); + + expect(wrapper.findAllByTestId('user-achievement').length).toBe(0); + }); + + it('renders no achievements when none are present', async () => { + createComponent({ + queryHandler: jest.fn().mockResolvedValue(getUserAchievementsEmptyResponse), + }); + + await waitForPromises(); + + expect(wrapper.findAllByTestId('user-achievement').length).toBe(0); + }); + + it('only renders 3 achievements when more are present', async () => { + createComponent({ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsLongResponse) }); + + await waitForPromises(); + + expect(wrapper.findAllByTestId('user-achievement').length).toBe(3); + }); + + it('renders correctly if the achievement is from a private namespace', async () => { + createComponent({ + queryHandler: jest.fn().mockResolvedValue(getUserAchievementsPrivateGroupResponse), + }); + + await waitForPromises(); + + const userAchievement = + getUserAchievementsPrivateGroupResponse.data.user.userAchievements.nodes[0]; + + expect(achievement().text()).toContain(userAchievement.achievement.name); + expect(achievement().text()).toContain( + `Awarded ${getTimeago().format( + userAchievement.createdAt, + timeagoLanguageCode, + )} by a private namespace`, + ); + }); + + it('renders achievement correctly', async () => { + createComponent(); + + await waitForPromises(); + + expect(achievement().text()).toContain(userAchievement1.achievement.name); + expect(achievement().text()).toContain( + `Awarded ${getTimeago().format(userAchievement1.createdAt, timeagoLanguageCode)} by`, + ); + expect(achievement().text()).toContain(userAchievement1.achievement.namespace.fullPath); + expect(achievement().text()).toContain(userAchievement1.achievement.description); + expect(achievement().find('img').attributes('src')).toBe( + userAchievement1.achievement.avatarUrl, + ); + }); + + it('renders a placeholder when no avatar is present', async () => { + gon.gitlab_logo = PLACEHOLDER_URL; + createComponent({ + queryHandler: jest.fn().mockResolvedValue(getUserAchievementsNoAvatarResponse), + }); + + await waitForPromises(); + + expect(achievement().find('img').attributes('src')).toBe(PLACEHOLDER_URL); + }); + + it('does not render a description when none is present', async () => { + gon.gitlab_logo = PLACEHOLDER_URL; + createComponent({ + queryHandler: jest.fn().mockResolvedValue(getUserAchievementsNoAvatarResponse), + }); + + await waitForPromises(); + + expect(wrapper.findAllByTestId('achievement-description').length).toBe(0); + }); +}); |