diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 12:55:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 12:55:51 +0300 |
commit | e8d2c2579383897a1dd7f9debd359abe8ae8373d (patch) | |
tree | c42be41678c2586d49a75cabce89322082698334 /spec/frontend/admin | |
parent | fc845b37ec3a90aaa719975f607740c22ba6a113 (diff) |
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'spec/frontend/admin')
9 files changed, 492 insertions, 51 deletions
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js index 5db5b8a90a9..67d9bac8580 100644 --- a/spec/frontend/admin/users/components/actions/actions_spec.js +++ b/spec/frontend/admin/users/components/actions/actions_spec.js @@ -39,37 +39,12 @@ describe('Action components', () => { await nextTick(); - const div = wrapper.find('div'); - expect(div.attributes('data-path')).toBe('/test'); - expect(div.attributes('data-modal-attributes')).toContain('John Doe'); + expect(wrapper.attributes('data-path')).toBe('/test'); + expect(wrapper.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', () => { const oncallSchedules = [{ name: 'schedule1' }, { name: 'schedule2' }]; it.each(DELETE_ACTIONS)('renders a dropdown item for "%s"', async (action) => { diff --git a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap new file mode 100644 index 00000000000..5e367891337 --- /dev/null +++ b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`User Operation confirmation modal renders modal with form included 1`] = ` +<div> + <p> + <gl-sprintf-stub + message="content" + /> + </p> + + <oncall-schedules-list-stub + schedules="schedule1,schedule2" + username="username" + /> + + <p> + <gl-sprintf-stub + message="To confirm, type %{username}" + /> + </p> + + <form + action="delete-url" + method="post" + > + <input + name="_method" + type="hidden" + value="delete" + /> + + <input + name="authenticity_token" + type="hidden" + value="csrf" + /> + + <gl-form-input-stub + autocomplete="off" + autofocus="" + name="username" + type="text" + value="" + /> + </form> + <gl-button-stub + buttontextclasses="" + category="primary" + icon="" + size="medium" + variant="default" + > + Cancel + </gl-button-stub> + + <gl-button-stub + buttontextclasses="" + category="secondary" + disabled="true" + icon="" + size="medium" + variant="danger" + > + + secondaryAction + + </gl-button-stub> + + <gl-button-stub + buttontextclasses="" + category="primary" + disabled="true" + icon="" + size="medium" + variant="danger" + > + action + </gl-button-stub> +</div> +`; diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js new file mode 100644 index 00000000000..fee74764645 --- /dev/null +++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js @@ -0,0 +1,167 @@ +import { GlButton, GlFormInput } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue'; +import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; +import ModalStub from './stubs/modal_stub'; + +const TEST_DELETE_USER_URL = 'delete-url'; +const TEST_BLOCK_USER_URL = 'block-url'; +const TEST_CSRF = 'csrf'; + +describe('User Operation confirmation modal', () => { + let wrapper; + let formSubmitSpy; + + const findButton = (variant, category) => + wrapper + .findAll(GlButton) + .filter((w) => w.attributes('variant') === variant && w.attributes('category') === category) + .at(0); + const findForm = () => wrapper.find('form'); + const findUsernameInput = () => wrapper.findComponent(GlFormInput); + const findPrimaryButton = () => findButton('danger', 'primary'); + const findSecondaryButton = () => findButton('danger', 'secondary'); + const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token'); + const getUsername = () => findUsernameInput().attributes('value'); + const getMethodParam = () => new FormData(findForm().element).get('_method'); + const getFormAction = () => findForm().attributes('action'); + const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList); + + const setUsername = (username) => { + findUsernameInput().vm.$emit('input', username); + }; + + const username = 'username'; + const badUsername = 'bad_username'; + const oncallSchedules = '["schedule1", "schedule2"]'; + + const createComponent = (props = {}) => { + wrapper = shallowMount(DeleteUserModal, { + propsData: { + username, + title: 'title', + content: 'content', + action: 'action', + secondaryAction: 'secondaryAction', + deleteUserUrl: TEST_DELETE_USER_URL, + blockUserUrl: TEST_BLOCK_USER_URL, + csrfToken: TEST_CSRF, + oncallSchedules, + ...props, + }, + stubs: { + GlModal: ModalStub, + }, + }); + }; + + beforeEach(() => { + formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders modal with form included', () => { + createComponent(); + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('on created', () => { + beforeEach(() => { + createComponent(); + }); + + it('has disabled buttons', () => { + expect(findPrimaryButton().attributes('disabled')).toBeTruthy(); + expect(findSecondaryButton().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('with incorrect username', () => { + beforeEach(() => { + createComponent(); + setUsername(badUsername); + + return wrapper.vm.$nextTick(); + }); + + it('shows incorrect username', () => { + expect(getUsername()).toEqual(badUsername); + }); + + it('has disabled buttons', () => { + expect(findPrimaryButton().attributes('disabled')).toBeTruthy(); + expect(findSecondaryButton().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('with correct username', () => { + beforeEach(() => { + createComponent(); + setUsername(username); + + return wrapper.vm.$nextTick(); + }); + + it('shows correct username', () => { + expect(getUsername()).toEqual(username); + }); + + it('has enabled buttons', () => { + expect(findPrimaryButton().attributes('disabled')).toBeFalsy(); + expect(findSecondaryButton().attributes('disabled')).toBeFalsy(); + }); + + describe('when primary action is submitted', () => { + beforeEach(() => { + findPrimaryButton().vm.$emit('click'); + + return wrapper.vm.$nextTick(); + }); + + it('clears the input', () => { + expect(getUsername()).toEqual(''); + }); + + it('has correct form attributes and calls submit', () => { + expect(getFormAction()).toBe(TEST_DELETE_USER_URL); + expect(getMethodParam()).toBe('delete'); + expect(findAuthenticityToken()).toBe(TEST_CSRF); + expect(formSubmitSpy).toHaveBeenCalled(); + }); + }); + + describe('when secondary action is submitted', () => { + beforeEach(() => { + findSecondaryButton().vm.$emit('click'); + + return wrapper.vm.$nextTick(); + }); + + it('has correct form attributes and calls submit', () => { + expect(getFormAction()).toBe(TEST_BLOCK_USER_URL); + expect(getMethodParam()).toBe('put'); + expect(findAuthenticityToken()).toBe(TEST_CSRF); + expect(formSubmitSpy).toHaveBeenCalled(); + }); + }); + }); + + describe('Related oncall-schedules list', () => { + it('does NOT render the list when user has no related schedules', () => { + createComponent({ oncallSchedules: '[]' }); + expect(findOnCallSchedulesList().exists()).toBe(false); + }); + + it('renders the list when user has related schedules', () => { + createComponent(); + + const schedules = findOnCallSchedulesList(); + expect(schedules.exists()).toBe(true); + expect(schedules.props('schedules')).toEqual(JSON.parse(oncallSchedules)); + }); + }); +}); diff --git a/spec/frontend/admin/users/components/modals/stubs/modal_stub.js b/spec/frontend/admin/users/components/modals/stubs/modal_stub.js new file mode 100644 index 00000000000..4dc55e909a0 --- /dev/null +++ b/spec/frontend/admin/users/components/modals/stubs/modal_stub.js @@ -0,0 +1,23 @@ +const ModalStub = { + inheritAttrs: false, + name: 'glmodal-stub', + data() { + return { + showWasCalled: false, + }; + }, + methods: { + show() { + this.showWasCalled = true; + }, + hide() {}, + }, + render(h) { + const children = [this.$slots.default, this.$slots['modal-footer']] + .filter(Boolean) + .reduce((acc, nodes) => acc.concat(nodes), []); + return h('div', children); + }, +}; + +export default ModalStub; diff --git a/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js b/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js new file mode 100644 index 00000000000..65ce242662b --- /dev/null +++ b/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js @@ -0,0 +1,126 @@ +import { mount } from '@vue/test-utils'; +import UserModalManager from '~/admin/users/components/modals/user_modal_manager.vue'; +import ModalStub from './stubs/modal_stub'; + +describe('Users admin page Modal Manager', () => { + let wrapper; + + const modalConfiguration = { + action1: { + title: 'action1', + content: 'Action Modal 1', + }, + action2: { + title: 'action2', + content: 'Action Modal 2', + }, + }; + + const findModal = () => wrapper.find({ ref: 'modal' }); + + const createComponent = (props = {}) => { + wrapper = mount(UserModalManager, { + propsData: { + selector: '.js-delete-user-modal-button', + modalConfiguration, + csrfToken: 'dummyCSRF', + ...props, + }, + stubs: { + DeleteUserModal: ModalStub, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('render behavior', () => { + it('does not renders modal when initialized', () => { + createComponent(); + expect(findModal().exists()).toBeFalsy(); + }); + + it('throws if action has no proper configuration', () => { + createComponent({ + modalConfiguration: {}, + }); + expect(() => wrapper.vm.show({ glModalAction: 'action1' })).toThrow(); + }); + + it('renders modal with expected props when valid configuration is passed', () => { + createComponent(); + wrapper.vm.show({ + glModalAction: 'action1', + extraProp: 'extraPropValue', + }); + + return wrapper.vm.$nextTick().then(() => { + const modal = findModal(); + expect(modal.exists()).toBeTruthy(); + expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF'); + expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue'); + expect(modal.vm.showWasCalled).toBeTruthy(); + }); + }); + }); + + describe('click handling', () => { + let button; + let button2; + + const createButtons = () => { + button = document.createElement('button'); + button2 = document.createElement('button'); + button.setAttribute('class', 'js-delete-user-modal-button'); + button.setAttribute('data-username', 'foo'); + button.setAttribute('data-gl-modal-action', 'action1'); + button.setAttribute('data-block-user-url', '/block'); + button.setAttribute('data-delete-user-url', '/delete'); + document.body.appendChild(button); + document.body.appendChild(button2); + }; + const removeButtons = () => { + button.remove(); + button = null; + button2.remove(); + button2 = null; + }; + + beforeEach(() => { + createButtons(); + createComponent(); + }); + + afterEach(() => { + removeButtons(); + }); + + it('renders the modal when the button is clicked', async () => { + button.click(); + + await wrapper.vm.$nextTick(); + + expect(findModal().exists()).toBe(true); + }); + + it('does not render the modal when a misconfigured button is clicked', async () => { + button.removeAttribute('data-gl-modal-action'); + button.click(); + + await wrapper.vm.$nextTick(); + + expect(findModal().exists()).toBe(false); + }); + + it('does not render the modal when a button without the selector class is clicked', async () => { + button2.click(); + + await wrapper.vm.$nextTick(); + + expect(findModal().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js index debe964e7aa..43313424553 100644 --- a/spec/frontend/admin/users/components/user_actions_spec.js +++ b/spec/frontend/admin/users/components/user_actions_spec.js @@ -1,4 +1,5 @@ import { GlDropdownDivider } from '@gitlab/ui'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import Actions from '~/admin/users/components/actions'; import AdminUserActions from '~/admin/users/components/user_actions.vue'; @@ -6,7 +7,7 @@ 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 { CONFIRMATION_ACTIONS, DELETE_ACTIONS, LDAP, EDIT } from '../constants'; import { users, paths } from '../mock_data'; describe('AdminUserActions component', () => { @@ -20,7 +21,7 @@ describe('AdminUserActions component', () => { findUserActions(id).find('[data-testid="dropdown-toggle"]'); const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider); - const initComponent = ({ actions = [] } = {}) => { + const initComponent = ({ actions = [], showButtonLabels } = {}) => { wrapper = shallowMountExtended(AdminUserActions, { propsData: { user: { @@ -28,6 +29,10 @@ describe('AdminUserActions component', () => { actions, }, paths, + showButtonLabels, + }, + directives: { + GlTooltip: createMockDirective(), }, }); }; @@ -62,7 +67,7 @@ describe('AdminUserActions component', () => { describe('actions dropdown', () => { describe('when there are actions', () => { - const actions = [EDIT, ...LINK_ACTIONS]; + const actions = [EDIT, ...CONFIRMATION_ACTIONS]; beforeEach(() => { initComponent({ actions }); @@ -72,19 +77,6 @@ describe('AdminUserActions component', () => { 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 }); @@ -157,4 +149,42 @@ describe('AdminUserActions component', () => { }); }); }); + + describe('when `showButtonLabels` prop is `false`', () => { + beforeEach(() => { + initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS] }); + }); + + it('does not render "Edit" button label', () => { + const tooltip = getBinding(findEditButton().element, 'gl-tooltip'); + + expect(findEditButton().text()).toBe(''); + expect(findEditButton().attributes('aria-label')).toBe(I18N_USER_ACTIONS.edit); + expect(tooltip).toBeDefined(); + expect(tooltip.value).toBe(I18N_USER_ACTIONS.edit); + }); + + it('does not render "User administration" dropdown button label', () => { + expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration); + expect(findActionsDropdown().props('textSrOnly')).toBe(true); + }); + }); + + describe('when `showButtonLabels` prop is `true`', () => { + beforeEach(() => { + initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS], showButtonLabels: true }); + }); + + it('renders "Edit" button label', () => { + const tooltip = getBinding(findEditButton().element, 'gl-tooltip'); + + expect(findEditButton().text()).toBe(I18N_USER_ACTIONS.edit); + expect(tooltip).not.toBeDefined(); + }); + + it('renders "User administration" dropdown button label', () => { + expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration); + expect(findActionsDropdown().props('textSrOnly')).toBe(false); + }); + }); }); diff --git a/spec/frontend/admin/users/constants.js b/spec/frontend/admin/users/constants.js index 60abdc6c248..d341eb03b1b 100644 --- a/spec/frontend/admin/users/constants.js +++ b/spec/frontend/admin/users/constants.js @@ -7,13 +7,23 @@ const ACTIVATE = 'activate'; const DEACTIVATE = 'deactivate'; const REJECT = 'reject'; const APPROVE = 'approve'; +const BAN = 'ban'; +const UNBAN = 'unban'; 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 CONFIRMATION_ACTIONS = [ + ACTIVATE, + BLOCK, + DEACTIVATE, + UNLOCK, + UNBLOCK, + BAN, + UNBAN, + APPROVE, + REJECT, +]; 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 20b60bd8640..06dbadd6d3d 100644 --- a/spec/frontend/admin/users/index_spec.js +++ b/spec/frontend/admin/users/index_spec.js @@ -1,7 +1,8 @@ import { createWrapper } from '@vue/test-utils'; -import { initAdminUsersApp } from '~/admin/users'; +import { initAdminUsersApp, initAdminUserActions } from '~/admin/users'; import AdminUsersApp from '~/admin/users/components/app.vue'; -import { users, paths } from './mock_data'; +import UserActions from '~/admin/users/components/user_actions.vue'; +import { users, user, paths } from './mock_data'; describe('initAdminUsersApp', () => { let wrapper; @@ -14,15 +15,12 @@ describe('initAdminUsersApp', () => { el.setAttribute('data-users', JSON.stringify(users)); el.setAttribute('data-paths', JSON.stringify(paths)); - document.body.appendChild(el); - wrapper = createWrapper(initAdminUsersApp(el)); }); afterEach(() => { wrapper.destroy(); wrapper = null; - el.remove(); el = null; }); @@ -33,3 +31,31 @@ describe('initAdminUsersApp', () => { }); }); }); + +describe('initAdminUserActions', () => { + let wrapper; + let el; + + const findUserActions = () => wrapper.find(UserActions); + + beforeEach(() => { + el = document.createElement('div'); + el.setAttribute('data-user', JSON.stringify(user)); + el.setAttribute('data-paths', JSON.stringify(paths)); + + wrapper = createWrapper(initAdminUserActions(el)); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + el = null; + }); + + it('parses and passes props', () => { + expect(findUserActions().props()).toMatchObject({ + user, + paths, + }); + }); +}); diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js index 4689ab36773..ded3e6f7edf 100644 --- a/spec/frontend/admin/users/mock_data.js +++ b/spec/frontend/admin/users/mock_data.js @@ -18,6 +18,8 @@ export const users = [ }, ]; +export const user = users[0]; + export const paths = { edit: '/admin/users/id/edit', approve: '/admin/users/id/approve', @@ -30,6 +32,8 @@ export const paths = { delete: '/admin/users/id', deleteWithContributions: '/admin/users/id', adminUser: '/admin/users/id', + ban: '/admin/users/id/ban', + unban: '/admin/users/id/unban', }; export const createGroupCountResponse = (groupCounts) => ({ |