351 lines
11 KiB
JavaScript
351 lines
11 KiB
JavaScript
import { GlBadge, GlPagination, GlTable } from '@gitlab/ui';
|
|
import Vue from 'vue';
|
|
import Vuex from 'vuex';
|
|
import setWindowLocation from 'helpers/set_window_location_helper';
|
|
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
|
|
import CreatedAt from '~/members/components/table/created_at.vue';
|
|
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
|
|
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
|
|
import MemberAvatar from '~/members/components/table/member_avatar.vue';
|
|
import MemberSource from '~/members/components/table/member_source.vue';
|
|
import MembersTable from '~/members/components/table/members_table.vue';
|
|
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
|
|
import UserDate from '~/vue_shared/components/user_date.vue';
|
|
import {
|
|
MEMBER_TYPES,
|
|
MEMBER_STATE_CREATED,
|
|
MEMBER_STATE_AWAITING,
|
|
MEMBER_STATE_ACTIVE,
|
|
USER_STATE_BLOCKED_PENDING_APPROVAL,
|
|
BADGE_LABELS_AWAITING_USER_SIGNUP,
|
|
BADGE_LABELS_PENDING_OWNER_APPROVAL,
|
|
TAB_QUERY_PARAM_VALUES,
|
|
} from '~/members/constants';
|
|
import * as initUserPopovers from '~/user_popovers';
|
|
import {
|
|
member as memberMock,
|
|
directMember,
|
|
invite,
|
|
accessRequest,
|
|
pagination,
|
|
} from '../../mock_data';
|
|
|
|
Vue.use(Vuex);
|
|
|
|
describe('MembersTable', () => {
|
|
let wrapper;
|
|
|
|
const createStore = (state = {}) => {
|
|
return new Vuex.Store({
|
|
modules: {
|
|
[MEMBER_TYPES.invite]: {
|
|
namespaced: true,
|
|
state: {
|
|
members: [],
|
|
tableFields: [],
|
|
tableAttrs: {
|
|
table: { 'data-qa-selector': 'members_list' },
|
|
tr: { 'data-qa-selector': 'member_row' },
|
|
},
|
|
pagination,
|
|
...state,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
const createComponent = (state, provide = {}) => {
|
|
wrapper = mountExtended(MembersTable, {
|
|
propsData: {
|
|
tabQueryParamValue: TAB_QUERY_PARAM_VALUES.invite,
|
|
},
|
|
store: createStore(state),
|
|
provide: {
|
|
sourceId: 1,
|
|
currentUserId: 1,
|
|
namespace: MEMBER_TYPES.invite,
|
|
...provide,
|
|
},
|
|
stubs: [
|
|
'member-avatar',
|
|
'member-source',
|
|
'created-at',
|
|
'member-action-buttons',
|
|
'role-dropdown',
|
|
'remove-group-link-modal',
|
|
'remove-member-modal',
|
|
'expiration-datepicker',
|
|
],
|
|
});
|
|
};
|
|
|
|
const url = 'https://localhost/foo-bar/-/project_members?tab=invited';
|
|
|
|
const findTable = () => wrapper.find(GlTable);
|
|
const findTableCellByMemberId = (tableCellLabel, memberId) =>
|
|
wrapper
|
|
.findByTestId(`members-table-row-${memberId}`)
|
|
.find(`[data-label="${tableCellLabel}"][role="cell"]`);
|
|
|
|
const findPagination = () => extendedWrapper(wrapper.find(GlPagination));
|
|
|
|
const expectCorrectLinkToPage2 = () => {
|
|
expect(findPagination().findByText('2', { selector: 'a' }).attributes('href')).toBe(
|
|
`${url}&invited_members_page=2`,
|
|
);
|
|
};
|
|
|
|
afterEach(() => {
|
|
wrapper.destroy();
|
|
});
|
|
|
|
describe('fields', () => {
|
|
const memberCanUpdate = {
|
|
...directMember,
|
|
canUpdate: true,
|
|
};
|
|
|
|
it.each`
|
|
field | label | member | expectedComponent
|
|
${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
|
|
${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
|
|
${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
|
|
${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
|
|
${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
|
|
${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
|
|
${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
|
|
${'userCreatedAt'} | ${'Created on'} | ${memberMock} | ${UserDate}
|
|
${'lastActivityOn'} | ${'Last activity'} | ${memberMock} | ${UserDate}
|
|
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
|
|
createComponent({
|
|
members: [member],
|
|
tableFields: [field],
|
|
});
|
|
|
|
expect(wrapper.findByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true);
|
|
|
|
if (expectedComponent) {
|
|
expect(
|
|
wrapper.find(`[data-label="${label}"][role="cell"]`).find(expectedComponent).exists(),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
describe('Invited column', () => {
|
|
describe.each`
|
|
state | userState | expectedBadgeLabel
|
|
${MEMBER_STATE_CREATED} | ${null} | ${BADGE_LABELS_AWAITING_USER_SIGNUP}
|
|
${MEMBER_STATE_CREATED} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
|
|
${MEMBER_STATE_AWAITING} | ${''} | ${BADGE_LABELS_AWAITING_USER_SIGNUP}
|
|
${MEMBER_STATE_AWAITING} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
|
|
${MEMBER_STATE_AWAITING} | ${'something_else'} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
|
|
${MEMBER_STATE_ACTIVE} | ${null} | ${''}
|
|
${MEMBER_STATE_ACTIVE} | ${'something_else'} | ${''}
|
|
`('Invited Badge', ({ state, userState, expectedBadgeLabel }) => {
|
|
it(`${
|
|
expectedBadgeLabel ? 'shows' : 'hides'
|
|
} invited badge if user status: '${userState}' and member state: '${state}'`, () => {
|
|
createComponent({
|
|
members: [
|
|
{
|
|
...invite,
|
|
state,
|
|
invite: {
|
|
...invite.invite,
|
|
userState,
|
|
},
|
|
},
|
|
],
|
|
tableFields: ['invited'],
|
|
});
|
|
|
|
const invitedTab = wrapper.findByTestId('invited-badge');
|
|
|
|
if (expectedBadgeLabel) {
|
|
expect(invitedTab.text()).toBe(expectedBadgeLabel);
|
|
} else {
|
|
expect(invitedTab.exists()).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('"Actions" field', () => {
|
|
it('renders "Actions" field for screen readers', () => {
|
|
createComponent({ members: [memberCanUpdate], tableFields: ['actions'] });
|
|
|
|
const actionField = wrapper.findByTestId('col-actions');
|
|
|
|
expect(actionField.exists()).toBe(true);
|
|
expect(actionField.classes('gl-sr-only')).toBe(true);
|
|
expect(
|
|
wrapper.find(`[data-label="Actions"][role="cell"]`).find(MemberActionButtons).exists(),
|
|
).toBe(true);
|
|
});
|
|
|
|
describe('when user is not logged in', () => {
|
|
it('does not render the "Actions" field', () => {
|
|
createComponent({ tableFields: ['actions'] }, { currentUserId: null });
|
|
|
|
expect(wrapper.findByTestId('col-actions').exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
const memberCanRemove = {
|
|
...directMember,
|
|
canRemove: true,
|
|
};
|
|
|
|
const memberNoPermissions = {
|
|
...memberMock,
|
|
id: 2,
|
|
};
|
|
|
|
describe.each`
|
|
permission | members
|
|
${'canUpdate'} | ${[memberNoPermissions, memberCanUpdate]}
|
|
${'canRemove'} | ${[memberNoPermissions, memberCanRemove]}
|
|
${'canResend'} | ${[memberNoPermissions, invite]}
|
|
`('when one of the members has $permission permissions', ({ members }) => {
|
|
it('renders the "Actions" field', () => {
|
|
createComponent({ members, tableFields: ['actions'] });
|
|
|
|
expect(wrapper.findByTestId('col-actions').exists()).toBe(true);
|
|
|
|
expect(findTableCellByMemberId('Actions', members[0].id).classes()).toStrictEqual([
|
|
'col-actions',
|
|
'gl-display-none!',
|
|
'gl-lg-display-table-cell!',
|
|
]);
|
|
expect(findTableCellByMemberId('Actions', members[1].id).classes()).toStrictEqual([
|
|
'col-actions',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe.each`
|
|
permission | members
|
|
${'canUpdate'} | ${[memberMock]}
|
|
${'canRemove'} | ${[memberMock]}
|
|
${'canResend'} | ${[{ ...invite, invite: { ...invite.invite, canResend: false } }]}
|
|
`('when none of the members have $permission permissions', ({ members }) => {
|
|
it('does not render the "Actions" field', () => {
|
|
createComponent({ members, tableFields: ['actions'] });
|
|
|
|
expect(wrapper.findByTestId('col-actions').exists()).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when `members` is an empty array', () => {
|
|
it('displays a "No members found" message', () => {
|
|
createComponent();
|
|
|
|
expect(wrapper.findByText('No members found').exists()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('when member can not be updated', () => {
|
|
it('renders badge in "Max role" field', () => {
|
|
createComponent({ members: [memberMock], tableFields: ['maxRole'] });
|
|
|
|
expect(wrapper.find(`[data-label="Max role"][role="cell"]`).find(GlBadge).text()).toBe(
|
|
memberMock.accessLevel.stringValue,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('initializes user popovers when mounted', () => {
|
|
const initUserPopoversMock = jest.spyOn(initUserPopovers, 'default');
|
|
|
|
createComponent();
|
|
|
|
expect(initUserPopoversMock).toHaveBeenCalled();
|
|
});
|
|
|
|
it('adds QA selector to table', () => {
|
|
createComponent();
|
|
|
|
expect(findTable().attributes('data-qa-selector')).toBe('members_list');
|
|
});
|
|
|
|
it('adds QA selector to table row', () => {
|
|
createComponent();
|
|
|
|
expect(findTable().find('tbody tr').attributes('data-qa-selector')).toBe('member_row');
|
|
});
|
|
|
|
describe('when required pagination data is provided', () => {
|
|
it('renders `gl-pagination` component with correct props', () => {
|
|
setWindowLocation(url);
|
|
|
|
createComponent();
|
|
|
|
const glPagination = findPagination();
|
|
|
|
expect(glPagination.exists()).toBe(true);
|
|
expect(glPagination.props()).toMatchObject({
|
|
value: pagination.currentPage,
|
|
perPage: pagination.perPage,
|
|
totalItems: pagination.totalItems,
|
|
prevText: 'Prev',
|
|
nextText: 'Next',
|
|
labelNextPage: 'Go to next page',
|
|
labelPrevPage: 'Go to previous page',
|
|
align: 'center',
|
|
});
|
|
});
|
|
|
|
it('uses `pagination.paramName` to generate the pagination links', () => {
|
|
setWindowLocation(url);
|
|
|
|
createComponent({
|
|
pagination: {
|
|
currentPage: 1,
|
|
perPage: 5,
|
|
totalItems: 10,
|
|
paramName: 'invited_members_page',
|
|
},
|
|
});
|
|
|
|
expectCorrectLinkToPage2();
|
|
});
|
|
|
|
it('removes any url params defined as `null` in the `params` attribute', () => {
|
|
setWindowLocation(`${url}&search_groups=foo`);
|
|
|
|
createComponent({
|
|
pagination: {
|
|
currentPage: 1,
|
|
perPage: 5,
|
|
totalItems: 10,
|
|
paramName: 'invited_members_page',
|
|
params: { search_groups: null },
|
|
},
|
|
});
|
|
|
|
expectCorrectLinkToPage2();
|
|
});
|
|
});
|
|
|
|
describe.each`
|
|
attribute | value
|
|
${'paramName'} | ${null}
|
|
${'currentPage'} | ${null}
|
|
${'perPage'} | ${null}
|
|
${'totalItems'} | ${0}
|
|
`('when pagination.$attribute is $value', ({ attribute, value }) => {
|
|
it('does not render `gl-pagination`', () => {
|
|
createComponent({
|
|
pagination: {
|
|
...pagination,
|
|
[attribute]: value,
|
|
},
|
|
});
|
|
|
|
expect(findPagination().exists()).toBe(false);
|
|
});
|
|
});
|
|
});
|