diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-18 10:34:06 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-18 10:34:06 +0000 |
commit | 859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch) | |
tree | d7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /spec/frontend/admin/users | |
parent | 446d496a6d000c73a304be52587cd9bbc7493136 (diff) | |
download | gitlab-ce-859a6fb938bb9ee2a317c46dfa4fcc1af49608f0.tar.gz |
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'spec/frontend/admin/users')
-rw-r--r-- | spec/frontend/admin/users/components/actions/actions_spec.js | 98 | ||||
-rw-r--r-- | spec/frontend/admin/users/components/user_actions_spec.js | 158 | ||||
-rw-r--r-- | spec/frontend/admin/users/components/user_avatar_spec.js | 85 | ||||
-rw-r--r-- | spec/frontend/admin/users/components/user_date_spec.js | 34 | ||||
-rw-r--r-- | spec/frontend/admin/users/components/users_table_spec.js | 28 | ||||
-rw-r--r-- | spec/frontend/admin/users/constants.js | 19 | ||||
-rw-r--r-- | spec/frontend/admin/users/index_spec.js | 4 | ||||
-rw-r--r-- | spec/frontend/admin/users/mock_data.js | 1 |
8 files changed, 403 insertions, 24 deletions
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', }, ]; |