diff options
Diffstat (limited to 'spec/frontend/admin')
12 files changed, 410 insertions, 31 deletions
diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js index a4dcfa1a480..9c424491d04 100644 --- a/spec/frontend/admin/statistics_panel/components/app_spec.js +++ b/spec/frontend/admin/statistics_panel/components/app_spec.js @@ -1,12 +1,12 @@ -import Vuex from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; -import { GlLoadingIcon } from '@gitlab/ui'; -import axios from '~/lib/utils/axios_utils'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import Vuex from 'vuex'; import StatisticsPanelApp from '~/admin/statistics_panel/components/app.vue'; import statisticsLabels from '~/admin/statistics_panel/constants'; import createStore from '~/admin/statistics_panel/store'; +import axios from '~/lib/utils/axios_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import mockStatistics from '../mock_data'; const localVue = createLocalVue(); diff --git a/spec/frontend/admin/statistics_panel/store/actions_spec.js b/spec/frontend/admin/statistics_panel/store/actions_spec.js index ecbc823be12..c7481b664b3 100644 --- a/spec/frontend/admin/statistics_panel/store/actions_spec.js +++ b/spec/frontend/admin/statistics_panel/store/actions_spec.js @@ -1,10 +1,10 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import * as actions from '~/admin/statistics_panel/store/actions'; import * as types from '~/admin/statistics_panel/store/mutation_types'; import getInitialState from '~/admin/statistics_panel/store/state'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import mockStatistics from '../mock_data'; describe('Admin statistics panel actions', () => { diff --git a/spec/frontend/admin/statistics_panel/store/getters_spec.js b/spec/frontend/admin/statistics_panel/store/getters_spec.js index 152d82531ed..6cdd40b1a98 100644 --- a/spec/frontend/admin/statistics_panel/store/getters_spec.js +++ b/spec/frontend/admin/statistics_panel/store/getters_spec.js @@ -1,5 +1,5 @@ -import createState from '~/admin/statistics_panel/store/state'; import * as getters from '~/admin/statistics_panel/store/getters'; +import createState from '~/admin/statistics_panel/store/state'; describe('Admin statistics panel getters', () => { let state; diff --git a/spec/frontend/admin/statistics_panel/store/mutations_spec.js b/spec/frontend/admin/statistics_panel/store/mutations_spec.js index 179f38d2bc5..0a3dad09c9a 100644 --- a/spec/frontend/admin/statistics_panel/store/mutations_spec.js +++ b/spec/frontend/admin/statistics_panel/store/mutations_spec.js @@ -1,5 +1,5 @@ -import mutations from '~/admin/statistics_panel/store/mutations'; import * as types from '~/admin/statistics_panel/store/mutation_types'; +import mutations from '~/admin/statistics_panel/store/mutations'; import getInitialState from '~/admin/statistics_panel/store/state'; import mockStatistics from '../mock_data'; diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js new file mode 100644 index 00000000000..5e232f34311 --- /dev/null +++ b/spec/frontend/admin/users/components/actions/actions_spec.js @@ -0,0 +1,98 @@ +import { GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { kebabCase } from 'lodash'; +import { nextTick } from 'vue'; +import Actions from '~/admin/users/components/actions'; +import SharedDeleteAction from '~/admin/users/components/actions/shared/shared_delete_action.vue'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; + +import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants'; + +describe('Action components', () => { + let wrapper; + + const findDropdownItem = () => wrapper.find(GlDropdownItem); + + const initComponent = ({ component, props, stubs = {} } = {}) => { + wrapper = shallowMount(component, { + propsData: { + ...props, + }, + stubs, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('CONFIRMATION_ACTIONS', () => { + it.each(CONFIRMATION_ACTIONS)('renders a dropdown item for "%s"', async (action) => { + initComponent({ + component: Actions[capitalizeFirstCharacter(action)], + props: { + username: 'John Doe', + path: '/test', + }, + }); + + await nextTick(); + + const div = wrapper.find('div'); + expect(div.attributes('data-path')).toBe('/test'); + expect(div.attributes('data-modal-attributes')).toContain('John Doe'); + expect(findDropdownItem().exists()).toBe(true); + }); + }); + + describe('LINK_ACTIONS', () => { + it.each` + action | method + ${'Approve'} | ${'put'} + ${'Reject'} | ${'delete'} + `( + 'renders a dropdown item link with method "$method" for "$action"', + async ({ action, method }) => { + initComponent({ + component: Actions[action], + props: { + path: '/test', + }, + }); + + await nextTick(); + + const item = wrapper.find(GlDropdownItem); + expect(item.attributes('href')).toBe('/test'); + expect(item.attributes('data-method')).toContain(method); + }, + ); + }); + + describe('DELETE_ACTION_COMPONENTS', () => { + it.each(DELETE_ACTIONS)('renders a dropdown item for "%s"', async (action) => { + initComponent({ + component: Actions[capitalizeFirstCharacter(action)], + props: { + username: 'John Doe', + paths: { + delete: '/delete', + block: '/block', + }, + }, + stubs: { SharedDeleteAction }, + }); + + await nextTick(); + + const sharedAction = wrapper.find(SharedDeleteAction); + + expect(sharedAction.attributes('data-block-user-url')).toBe('/block'); + expect(sharedAction.attributes('data-delete-user-url')).toBe('/delete'); + expect(sharedAction.attributes('data-gl-modal-action')).toBe(kebabCase(action)); + expect(sharedAction.attributes('data-username')).toBe('John Doe'); + expect(findDropdownItem().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js new file mode 100644 index 00000000000..0745d961f25 --- /dev/null +++ b/spec/frontend/admin/users/components/user_actions_spec.js @@ -0,0 +1,158 @@ +import { GlDropdownDivider } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Actions from '~/admin/users/components/actions'; +import AdminUserActions from '~/admin/users/components/user_actions.vue'; +import { I18N_USER_ACTIONS } from '~/admin/users/constants'; +import { generateUserPaths } from '~/admin/users/utils'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; + +import { CONFIRMATION_ACTIONS, DELETE_ACTIONS, LINK_ACTIONS, LDAP, EDIT } from '../constants'; +import { users, paths } from '../mock_data'; + +describe('AdminUserActions component', () => { + let wrapper; + const user = users[0]; + const userPaths = generateUserPaths(paths, user.username); + + const findEditButton = () => wrapper.find('[data-testid="edit"]'); + const findActionsDropdown = () => wrapper.find('[data-testid="actions"'); + const findDropdownDivider = () => wrapper.find(GlDropdownDivider); + + const initComponent = ({ actions = [] } = {}) => { + wrapper = shallowMount(AdminUserActions, { + propsData: { + user: { + ...user, + actions, + }, + paths, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('edit button', () => { + describe('when the user has an edit action attached', () => { + beforeEach(() => { + initComponent({ actions: [EDIT] }); + }); + + it('renders the edit button linking to the user edit path', () => { + expect(findEditButton().exists()).toBe(true); + expect(findEditButton().attributes('href')).toBe(userPaths.edit); + }); + }); + + describe('when there is no edit action attached to the user', () => { + beforeEach(() => { + initComponent({ actions: [] }); + }); + + it('does not render the edit button linking to the user edit path', () => { + expect(findEditButton().exists()).toBe(false); + }); + }); + }); + + describe('actions dropdown', () => { + describe('when there are actions', () => { + const actions = [EDIT, ...LINK_ACTIONS]; + + beforeEach(() => { + initComponent({ actions }); + }); + + it('renders the actions dropdown', () => { + expect(findActionsDropdown().exists()).toBe(true); + }); + + describe('when there are actions that should render as links', () => { + beforeEach(() => { + initComponent({ actions: LINK_ACTIONS }); + }); + + it.each(LINK_ACTIONS)('renders an action component item for "%s"', (action) => { + const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]); + + expect(component.props('path')).toBe(userPaths[action]); + expect(component.text()).toBe(I18N_USER_ACTIONS[action]); + }); + }); + + describe('when there are actions that require confirmation', () => { + beforeEach(() => { + initComponent({ actions: CONFIRMATION_ACTIONS }); + }); + + it.each(CONFIRMATION_ACTIONS)('renders an action component item for "%s"', (action) => { + const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]); + + expect(component.props('username')).toBe(user.name); + expect(component.props('path')).toBe(userPaths[action]); + expect(component.text()).toBe(I18N_USER_ACTIONS[action]); + }); + }); + + describe('when there is a LDAP action', () => { + beforeEach(() => { + initComponent({ actions: [LDAP] }); + }); + + it('renders the LDAP dropdown item without a link', () => { + const dropdownAction = wrapper.find(`[data-testid="${LDAP}"]`); + expect(dropdownAction.exists()).toBe(true); + expect(dropdownAction.attributes('href')).toBe(undefined); + expect(dropdownAction.text()).toBe(I18N_USER_ACTIONS[LDAP]); + }); + }); + + describe('when there is a delete action', () => { + beforeEach(() => { + initComponent({ actions: [LDAP, ...DELETE_ACTIONS] }); + }); + + it('renders a dropdown divider', () => { + expect(findDropdownDivider().exists()).toBe(true); + }); + + it('only renders delete dropdown items for actions containing the word "delete"', () => { + const { length } = wrapper.findAll(`[data-testid*="delete-"]`); + expect(length).toBe(DELETE_ACTIONS.length); + }); + + it.each(DELETE_ACTIONS)('renders a delete action component item for "%s"', (action) => { + const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]); + + expect(component.props('username')).toBe(user.name); + expect(component.props('paths')).toEqual(userPaths); + expect(component.text()).toBe(I18N_USER_ACTIONS[action]); + }); + }); + + describe('when there are no delete actions', () => { + it('does not render a dropdown divider', () => { + expect(findDropdownDivider().exists()).toBe(false); + }); + + it('does not render a delete dropdown item', () => { + const anyDeleteAction = wrapper.find(`[data-testid*="delete-"]`); + expect(anyDeleteAction.exists()).toBe(false); + }); + }); + }); + + describe('when there are no actions', () => { + beforeEach(() => { + initComponent({ actions: [] }); + }); + + it('does not render the actions dropdown', () => { + expect(findActionsDropdown().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/admin/users/components/user_avatar_spec.js index ba4e83690d0..8bbfb89bec1 100644 --- a/spec/frontend/admin/users/components/user_avatar_spec.js +++ b/spec/frontend/admin/users/components/user_avatar_spec.js @@ -1,7 +1,10 @@ -import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import AdminUserAvatar from '~/admin/users/components/user_avatar.vue'; +import { LENGTH_OF_USER_NOTE_TOOLTIP } from '~/admin/users/constants'; +import { truncate } from '~/lib/utils/text_utility'; import { users, paths } from '../mock_data'; describe('AdminUserAvatar component', () => { @@ -9,17 +12,25 @@ describe('AdminUserAvatar component', () => { const user = users[0]; const adminUserPath = paths.adminUser; + const findNote = () => wrapper.find(GlIcon); const findAvatar = () => wrapper.find(GlAvatarLabeled); - const findAvatarLink = () => wrapper.find(GlAvatarLink); + const findUserLink = () => wrapper.find('.js-user-link'); const findAllBadges = () => wrapper.findAll(GlBadge); + const findTooltip = () => getBinding(findNote().element, 'gl-tooltip'); const initComponent = (props = {}) => { - wrapper = mount(AdminUserAvatar, { + wrapper = shallowMount(AdminUserAvatar, { propsData: { user, adminUserPath, ...props, }, + directives: { + GlTooltip: createMockDirective(), + }, + stubs: { + GlAvatarLabeled, + }, }); }; @@ -33,31 +44,83 @@ describe('AdminUserAvatar component', () => { initComponent(); }); - it("links to the user's admin path", () => { - expect(findAvatarLink().attributes()).toMatchObject({ - href: adminUserPath.replace('id', user.username), + it('adds a user link hover card', () => { + expect(findUserLink().attributes()).toMatchObject({ 'data-user-id': user.id.toString(), 'data-username': user.username, }); }); - it("renders the user's name", () => { - expect(findAvatar().props('label')).toBe(user.name); + it("renders the user's name with an admin path link", () => { + const avatar = findAvatar(); + + expect(avatar.props('label')).toBe(user.name); + expect(avatar.props('labelLink')).toBe(adminUserPath.replace('id', user.username)); }); - it("renders the user's email", () => { - expect(findAvatar().props('subLabel')).toBe(user.email); + it("renders the user's email with a mailto link", () => { + const avatar = findAvatar(); + + expect(avatar.props('subLabel')).toBe(user.email); + expect(avatar.props('subLabelLink')).toBe(`mailto:${user.email}`); }); it("renders the user's avatar image", () => { expect(findAvatar().attributes('src')).toBe(user.avatarUrl); }); + it('renders a user note icon', () => { + expect(findNote().exists()).toBe(true); + expect(findNote().props('name')).toBe('document'); + }); + + it("renders the user's note tooltip", () => { + const tooltip = findTooltip(); + + expect(tooltip).toBeDefined(); + expect(tooltip.value).toBe(user.note); + }); + it("renders the user's badges", () => { findAllBadges().wrappers.forEach((badge, idx) => { expect(badge.text()).toBe(user.badges[idx].text); expect(badge.props('variant')).toBe(user.badges[idx].variant); }); }); + + describe('and the user note is very long', () => { + const noteText = new Array(LENGTH_OF_USER_NOTE_TOOLTIP + 1).join('a'); + + beforeEach(() => { + initComponent({ + user: { + ...user, + note: noteText, + }, + }); + }); + + it("renders a truncated user's note tooltip", () => { + const tooltip = findTooltip(); + + expect(tooltip).toBeDefined(); + expect(tooltip.value).toBe(truncate(noteText, LENGTH_OF_USER_NOTE_TOOLTIP)); + }); + }); + + describe('and the user does not have a note', () => { + beforeEach(() => { + initComponent({ + user: { + ...user, + note: null, + }, + }); + }); + + it('does not render a user note', () => { + expect(findNote().exists()).toBe(false); + }); + }); }); }); diff --git a/spec/frontend/admin/users/components/user_date_spec.js b/spec/frontend/admin/users/components/user_date_spec.js new file mode 100644 index 00000000000..6428b10059b --- /dev/null +++ b/spec/frontend/admin/users/components/user_date_spec.js @@ -0,0 +1,34 @@ +import { shallowMount } from '@vue/test-utils'; + +import UserDate from '~/admin/users/components/user_date.vue'; +import { users } from '../mock_data'; + +const mockDate = users[0].createdAt; + +describe('FormatDate component', () => { + let wrapper; + + const initComponent = (props = {}) => { + wrapper = shallowMount(UserDate, { + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it.each` + date | output + ${mockDate} | ${'13 Nov, 2020'} + ${null} | ${'Never'} + ${undefined} | ${'Never'} + `('renders $date as $output', ({ date, output }) => { + initComponent({ date }); + + expect(wrapper.text()).toBe(output); + }); +}); diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js index b79d2d4d39d..f1fcc20fb65 100644 --- a/spec/frontend/admin/users/components/users_table_spec.js +++ b/spec/frontend/admin/users/components/users_table_spec.js @@ -1,8 +1,11 @@ import { GlTable } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import AdminUsersTable from '~/admin/users/components/users_table.vue'; +import AdminUserActions from '~/admin/users/components/user_actions.vue'; import AdminUserAvatar from '~/admin/users/components/user_avatar.vue'; +import AdminUserDate from '~/admin/users/components/user_date.vue'; +import AdminUsersTable from '~/admin/users/components/users_table.vue'; + import { users, paths } from '../mock_data'; describe('AdminUsersTable component', () => { @@ -39,18 +42,21 @@ describe('AdminUsersTable component', () => { initComponent(); }); - it.each` - key | label - ${'name'} | ${'Name'} - ${'projectsCount'} | ${'Projects'} - ${'createdAt'} | ${'Created on'} - ${'lastActivityOn'} | ${'Last activity'} - `('renders users.$key in column $label', ({ key, label }) => { - expect(getCellByLabel(0, label).text()).toContain(`${user[key]}`); + it('renders the projects count', () => { + expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`); }); - it('renders an AdminUserAvatar component', () => { - expect(getCellByLabel(0, 'Name').find(AdminUserAvatar).exists()).toBe(true); + it('renders the user actions', () => { + expect(wrapper.find(AdminUserActions).exists()).toBe(true); + }); + + it.each` + component | label + ${AdminUserAvatar} | ${'Name'} + ${AdminUserDate} | ${'Created on'} + ${AdminUserDate} | ${'Last activity'} + `('renders the component for column $label', ({ component, label }) => { + expect(getCellByLabel(0, label).find(component).exists()).toBe(true); }); }); diff --git a/spec/frontend/admin/users/constants.js b/spec/frontend/admin/users/constants.js new file mode 100644 index 00000000000..60abdc6c248 --- /dev/null +++ b/spec/frontend/admin/users/constants.js @@ -0,0 +1,19 @@ +const BLOCK = 'block'; +const UNBLOCK = 'unblock'; +const DELETE = 'delete'; +const DELETE_WITH_CONTRIBUTIONS = 'deleteWithContributions'; +const UNLOCK = 'unlock'; +const ACTIVATE = 'activate'; +const DEACTIVATE = 'deactivate'; +const REJECT = 'reject'; +const APPROVE = 'approve'; + +export const EDIT = 'edit'; + +export const LDAP = 'ldapBlocked'; + +export const LINK_ACTIONS = [APPROVE, REJECT]; + +export const CONFIRMATION_ACTIONS = [ACTIVATE, BLOCK, DEACTIVATE, UNLOCK, UNBLOCK]; + +export const DELETE_ACTIONS = [DELETE, DELETE_WITH_CONTRIBUTIONS]; diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js index 171d54c8f4f..20b60bd8640 100644 --- a/spec/frontend/admin/users/index_spec.js +++ b/spec/frontend/admin/users/index_spec.js @@ -1,5 +1,5 @@ import { createWrapper } from '@vue/test-utils'; -import initAdminUsers from '~/admin/users'; +import { initAdminUsersApp } from '~/admin/users'; import AdminUsersApp from '~/admin/users/components/app.vue'; import { users, paths } from './mock_data'; @@ -16,7 +16,7 @@ describe('initAdminUsersApp', () => { document.body.appendChild(el); - wrapper = createWrapper(initAdminUsers(el)); + wrapper = createWrapper(initAdminUsersApp(el)); }); afterEach(() => { diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js index 860994a9152..c3918ef5173 100644 --- a/spec/frontend/admin/users/mock_data.js +++ b/spec/frontend/admin/users/mock_data.js @@ -14,6 +14,7 @@ export const users = [ ], projectsCount: 0, actions: [], + note: 'Create per issue #999', }, ]; |