Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 12:55:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 12:55:51 +0300
commite8d2c2579383897a1dd7f9debd359abe8ae8373d (patch)
treec42be41678c2586d49a75cabce89322082698334 /spec/frontend/admin
parentfc845b37ec3a90aaa719975f607740c22ba6a113 (diff)
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'spec/frontend/admin')
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js29
-rw-r--r--spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap80
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js167
-rw-r--r--spec/frontend/admin/users/components/modals/stubs/modal_stub.js23
-rw-r--r--spec/frontend/admin/users/components/modals/user_modal_manager_spec.js126
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js62
-rw-r--r--spec/frontend/admin/users/constants.js16
-rw-r--r--spec/frontend/admin/users/index_spec.js36
-rw-r--r--spec/frontend/admin/users/mock_data.js4
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) => ({