Skip to content
代码片段 群组 项目
未验证 提交 2072ce67 编辑于 作者: Shane Maglangit's avatar Shane Maglangit 提交者: GitLab
浏览文件

Move organization role change dropdown to drawer

上级 8355c50f
No related branches found
No related tags found
无相关合并请求
......@@ -9,6 +9,15 @@ export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
export const ACCESS_LEVEL_DEFAULT = 'default';
export const ACCESS_LEVEL_OWNER = 'owner';
// Matches `app/graphql/types/organizations/organization_user_access_level_enum.rb
export const ACCESS_LEVEL_DEFAULT_STRING = 'DEFAULT';
export const ACCESS_LEVEL_OWNER_STRING = 'OWNER';
export const ACCESS_LEVEL_LABEL = {
[ACCESS_LEVEL_DEFAULT_STRING]: __('User'),
[ACCESS_LEVEL_OWNER_STRING]: __('Owner'),
};
export const FORM_FIELD_NAME = 'name';
export const FORM_FIELD_ID = 'id';
export const FORM_FIELD_PATH = 'path';
......
<script>
import { GlCollapsibleListbox, GlDrawer, GlTooltipDirective } from '@gitlab/ui';
import UserAvatar from '~/vue_shared/components/users_table/user_avatar.vue';
import {
ACCESS_LEVEL_DEFAULT_STRING,
ACCESS_LEVEL_LABEL,
ACCESS_LEVEL_OWNER_STRING,
} from '~/organizations/shared/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import organizationUserUpdateMutation from '~/organizations/users/graphql/mutations/organization_user_update.mutation.graphql';
import { createAlert } from '~/alert';
export default {
name: 'UserDetailsDrawer',
components: {
GlCollapsibleListbox,
GlDrawer,
UserAvatar,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['paths'],
i18n: {
title: s__('Organization|Organization user details'),
roleListboxLabel: s__('Organization|Organization role'),
disabledRoleListboxTooltipText: s__('Organization|Organizations must have at least one owner.'),
errorMessage: s__(
'Organization|An error occurred updating the organization role. Please try again.',
),
successMessage: s__('Organization|Organization role was updated successfully.'),
},
roleListboxItems: [
{
text: ACCESS_LEVEL_LABEL[ACCESS_LEVEL_DEFAULT_STRING],
value: ACCESS_LEVEL_DEFAULT_STRING,
},
{
text: ACCESS_LEVEL_LABEL[ACCESS_LEVEL_OWNER_STRING],
value: ACCESS_LEVEL_OWNER_STRING,
},
],
props: {
user: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
initialAccessLevel: this.user?.accessLevel.stringValue,
selectedAccessLevel: this.user?.accessLevel.stringValue,
loading: false,
};
},
computed: {
drawerHeaderHeight() {
return getContentWrapperHeight();
},
roleListboxDisabled() {
return this.user?.isLastOwner;
},
roleListboxTooltip() {
return this.roleListboxDisabled ? this.$options.i18n.disabledRoleListboxTooltipText : null;
},
},
watch: {
user(value) {
this.initialAccessLevel = value?.accessLevel.stringValue;
this.selectedAccessLevel = value?.accessLevel.stringValue;
},
},
methods: {
setLoading(value) {
this.loading = value;
this.$emit('loading', value);
},
onUpdateSuccess() {
this.initialAccessLevel = this.selectedAccessLevel;
this.$toast.show(this.$options.i18n.successMessage);
this.$emit('role-change');
},
async onRoleSelect() {
this.setLoading(true);
try {
const {
data: {
organizationUserUpdate: { errors },
},
} = await this.$apollo.mutate({
mutation: organizationUserUpdateMutation,
variables: {
input: {
id: this.user.gid,
accessLevel: this.selectedAccessLevel,
},
},
});
if (errors.length) {
createAlert({ message: errors[0] });
return;
}
this.onUpdateSuccess();
} catch (error) {
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
} finally {
this.setLoading(false);
}
},
close() {
this.$emit('close');
},
},
DRAWER_Z_INDEX,
};
</script>
<template>
<gl-drawer
v-if="user"
open
header-sticky
:header-height="drawerHeaderHeight"
:z-index="$options.DRAWER_Z_INDEX"
@close="close"
>
<template #title>
<h4 class="gl-m-0">{{ $options.i18n.title }}</h4>
</template>
<template #default>
<div>
<user-avatar :user="user" :admin-user-path="paths.adminUser" />
</div>
<div>
<h5>{{ $options.i18n.roleListboxLabel }}</h5>
<div
v-gl-tooltip="{ disabled: !roleListboxTooltip, title: roleListboxTooltip }"
class="gl-rounded-base focus:gl-focus"
:tabindex="roleListboxDisabled && 0"
>
<gl-collapsible-listbox
v-model="selectedAccessLevel"
block
toggle-class="gl-form-input-xl"
:disabled="roleListboxDisabled"
:items="$options.roleListboxItems"
:loading="loading"
@select="onRoleSelect"
/>
</div>
</div>
</template>
</gl-drawer>
</template>
<script>
import {
GlLoadingIcon,
GlKeysetPagination,
GlCollapsibleListbox,
GlTooltipDirective,
} from '@gitlab/ui';
import { createAlert } from '~/alert';
import { GlButton, GlLoadingIcon, GlKeysetPagination, GlTooltipDirective } from '@gitlab/ui';
import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
import UserDetailsDrawer from '~/organizations/users/components/user_details_drawer.vue';
import {
FIELD_NAME,
FIELD_ORGANIZATION_ROLE,
FIELD_CREATED_AT,
FIELD_LAST_ACTIVITY_ON,
} from '~/vue_shared/components/users_table/constants';
import { ACCESS_LEVEL_DEFAULT, ACCESS_LEVEL_OWNER } from '~/organizations/shared/constants';
import {
ACCESS_LEVEL_DEFAULT,
ACCESS_LEVEL_OWNER,
ACCESS_LEVEL_LABEL,
} from '~/organizations/shared/constants';
import { __, s__ } from '~/locale';
import organizationUserUpdateMutation from '../graphql/mutations/organization_user_update.mutation.graphql';
export default {
name: 'UsersView',
......@@ -30,10 +28,11 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
GlButton,
GlLoadingIcon,
GlKeysetPagination,
GlCollapsibleListbox,
UsersTable,
UserDetailsDrawer,
},
inject: ['paths'],
roleListboxItems: [
......@@ -70,52 +69,22 @@ export default {
},
data() {
return {
roleListboxLoadingStates: [],
userDetailsDrawerActiveUser: null,
userDetailsDrawerLoading: false,
};
},
methods: {
async onRoleSelect(accessLevel, user) {
this.roleListboxLoadingStates.push(user.gid);
try {
const {
data: {
organizationUserUpdate: { errors },
},
} = await this.$apollo.mutate({
mutation: organizationUserUpdateMutation,
variables: {
input: {
id: user.gid,
accessLevel,
},
},
});
if (errors.length) {
createAlert({ message: errors[0] });
return;
}
this.$toast.show(this.$options.i18n.successMessage);
this.$emit('role-change');
} catch (error) {
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
} finally {
this.roleListboxLoadingStates.splice(this.roleListboxLoadingStates.indexOf(user.gid), 1);
}
setUserDetailsDrawerActiveUser(user) {
this.userDetailsDrawerActiveUser = user;
},
roleListboxItemText(accessLevel) {
return this.$options.roleListboxItems.find((item) => item.value === accessLevel).text;
setUserDetailsDrawerLoading(loading) {
this.userDetailsDrawerLoading = loading;
},
isRoleListboxDisabled(user) {
return user.isLastOwner;
onRoleChange() {
this.$emit('role-change');
},
roleListboxTooltip(user) {
return this.isRoleListboxDisabled(user)
? this.$options.i18n.disabledRoleListboxTooltipText
: null;
userAccessLevelLabel(user) {
return ACCESS_LEVEL_LABEL[user.accessLevel.stringValue];
},
},
};
......@@ -132,26 +101,25 @@ export default {
:column-widths="$options.usersTable.columnWidths"
>
<template #organization-role="{ user }">
<div
v-gl-tooltip="{ disabled: !roleListboxTooltip(user), title: roleListboxTooltip(user) }"
class="gl-rounded-base focus:gl-focus"
:tabindex="isRoleListboxDisabled(user) && 0"
<gl-button
class="gl-block"
variant="link"
:disabled="userDetailsDrawerLoading"
@click="setUserDetailsDrawerActiveUser(user)"
>
<gl-collapsible-listbox
:disabled="isRoleListboxDisabled(user)"
:selected="user.accessLevel.stringValue"
block
toggle-class="gl-form-input-xl"
:items="$options.roleListboxItems"
:loading="roleListboxLoadingStates.includes(user.gid)"
@select="onRoleSelect($event, user)"
/>
</div>
{{ userAccessLevelLabel(user) }}
</gl-button>
</template>
</users-table>
<div class="gl-flex gl-justify-center">
<gl-keyset-pagination v-bind="pageInfo" @prev="$emit('prev')" @next="$emit('next')" />
</div>
</template>
<user-details-drawer
:user="userDetailsDrawerActiveUser"
@loading="setUserDetailsDrawerLoading"
@close="setUserDetailsDrawerActiveUser(null)"
@role-change="onRoleChange"
/>
</div>
</template>
......@@ -38938,6 +38938,9 @@ msgstr ""
msgid "Organization|Organization successfully created."
msgstr ""
 
msgid "Organization|Organization user details"
msgstr ""
msgid "Organization|Organization visibility level"
msgstr ""
 
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlCollapsibleListbox, GlDrawer } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import organizationUserUpdateResponseWithErrors from 'test_fixtures/graphql/organizations/organization_user_update.mutation.graphql_with_errors.json';
import organizationUserUpdateResponse from 'test_fixtures/graphql/organizations/organization_user_update.mutation.graphql.json';
import organizationUserUpdateMutation from '~/organizations/users/graphql/mutations/organization_user_update.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { pageInfoMultiplePages } from 'jest/organizations/mock_data';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import UserDetailsDrawer from '~/organizations/users/components/user_details_drawer.vue';
import UserAvatar from '~/vue_shared/components/users_table/user_avatar.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import {
ACCESS_LEVEL_DEFAULT_STRING,
ACCESS_LEVEL_OWNER_STRING,
} from '~/organizations/shared/constants';
import { MOCK_PATHS, MOCK_USERS_FORMATTED } from '../mock_data';
Vue.use(VueApollo);
jest.mock('~/alert');
describe('UserDetailsDrawer', () => {
let wrapper;
let mockApollo;
const mockUser = MOCK_USERS_FORMATTED[0];
const successfulResponseHandler = jest.fn().mockResolvedValue(organizationUserUpdateResponse);
const mockToastShow = jest.fn();
const findGlDrawer = () => wrapper.findComponent(GlDrawer);
const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findUserAvatar = () => wrapper.findComponent(UserAvatar);
const selectRole = (value) => findGlCollapsibleListbox().vm.$emit('select', value);
const createComponent = ({ props = {}, handler = successfulResponseHandler } = {}) => {
mockApollo = createMockApollo([[organizationUserUpdateMutation, handler]]);
wrapper = shallowMount(UserDetailsDrawer, {
propsData: {
user: mockUser,
pageInfo: pageInfoMultiplePages,
...props,
},
provide: {
paths: MOCK_PATHS,
},
apolloProvider: mockApollo,
mocks: {
$toast: {
show: mockToastShow,
},
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
afterEach(() => {
mockApollo = null;
});
describe('when there is no active user', () => {
it('does not render drawer', () => {
createComponent({ props: { user: null } });
expect(findGlDrawer().exists()).toBe(false);
});
});
describe('when there is an active user', () => {
beforeEach(() => {
createComponent();
});
it('renders open drawer', () => {
expect(findGlDrawer().exists()).toBe(true);
expect(findGlDrawer().props('open')).toBe(true);
});
it('renders drawer title', () => {
expect(findGlDrawer().text()).toContain('Organization user details');
});
it('renders user avatar', () => {
expect(findUserAvatar().props()).toMatchObject({
user: mockUser,
adminUserPath: MOCK_PATHS.adminUser,
});
});
it('renders role listbox label', () => {
expect(findGlDrawer().text()).toContain('Organization role');
});
it('renders role listbox correct props', () => {
expect(findGlCollapsibleListbox().props()).toMatchObject({
items: UserDetailsDrawer.roleListboxItems,
selected: mockUser.accessLevel.stringValue,
disabled: false,
});
});
it('does not render disabled listbox tooltip', () => {
const tooltipContainer = findGlCollapsibleListbox().element.parentNode;
const tooltip = getBinding(tooltipContainer, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(true);
expect(tooltipContainer.getAttribute('tabindex')).toBe(null);
});
describe('when user is last owner of organization', () => {
beforeEach(() => {
createComponent({
props: {
loading: false,
user: { ...mockUser, isLastOwner: true },
},
});
});
it('renders listbox as disabled', () => {
expect(findGlCollapsibleListbox().props('disabled')).toBe(true);
});
it('renders tooltip and makes element focusable', () => {
const tooltipContainer = findGlCollapsibleListbox().element.parentNode;
const tooltip = getBinding(tooltipContainer, 'gl-tooltip');
expect(tooltip.value).toEqual({
title: 'Organizations must have at least one owner.',
disabled: false,
});
expect(tooltipContainer.getAttribute('tabindex')).toBe('0');
});
});
describe('when selecting new role', () => {
beforeEach(() => {
createComponent();
selectRole(ACCESS_LEVEL_DEFAULT_STRING);
});
it('calls GraphQL mutation with correct variables', () => {
expect(successfulResponseHandler).toHaveBeenCalledWith({
input: {
id: mockUser.gid,
accessLevel: ACCESS_LEVEL_DEFAULT_STRING,
},
});
});
it('sets listbox to loading', () => {
expect(findGlCollapsibleListbox().props('loading')).toBe(true);
});
it('emits loading start event', () => {
expect(wrapper.emitted('loading')[0]).toEqual([true]);
});
describe('when role update is successful', () => {
beforeEach(async () => {
await waitForPromises();
});
it('shows toast when GraphQL mutation is successful', () => {
expect(mockToastShow).toHaveBeenCalledWith('Organization role was updated successfully.');
});
it('emits role-change event', () => {
expect(wrapper.emitted('role-change')).toHaveLength(1);
});
it('emits loading end event', () => {
expect(wrapper.emitted('loading')[1]).toEqual([false]);
});
});
describe('when role update has a validation error', () => {
beforeEach(async () => {
const errorResponseHandler = jest
.fn()
.mockResolvedValue(organizationUserUpdateResponseWithErrors);
createComponent({
handler: errorResponseHandler,
props: { user: { ...mockUser, accessLevel: ACCESS_LEVEL_OWNER_STRING } },
});
selectRole(ACCESS_LEVEL_DEFAULT_STRING);
await waitForPromises();
});
it('creates an alert', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'You cannot change the access of the last owner from the organization',
});
});
});
describe('when role update has a network error', () => {
const error = new Error();
beforeEach(async () => {
const errorResponseHandler = jest.fn().mockRejectedValue(error);
createComponent({
handler: errorResponseHandler,
props: { user: { ...mockUser, accessLevel: ACCESS_LEVEL_OWNER_STRING } },
});
selectRole(ACCESS_LEVEL_DEFAULT_STRING);
await waitForPromises();
});
it('creates an alert', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred updating the organization role. Please try again.',
error,
captureError: true,
});
});
});
});
describe('when drawer is closed', () => {
it('emits close event', () => {
findGlDrawer().vm.$emit('close');
expect(wrapper.emitted('close')).toHaveLength(1);
});
});
});
});
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import { GlLoadingIcon, GlKeysetPagination, GlCollapsibleListbox } from '@gitlab/ui';
import organizationUserUpdateResponse from 'test_fixtures/graphql/organizations/organization_user_update.mutation.graphql.json';
import organizationUserUpdateResponseWithErrors from 'test_fixtures/graphql/organizations/organization_user_update.mutation.graphql_with_errors.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { GlLoadingIcon, GlKeysetPagination, GlButton } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import UserDetailsDrawer from '~/organizations/users/components/user_details_drawer.vue';
import UsersView from '~/organizations/users/components/users_view.vue';
import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
import organizationUserUpdateMutation from '~/organizations/users/graphql/mutations/organization_user_update.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/alert';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { pageInfoMultiplePages } from 'jest/organizations/mock_data';
import { ACCESS_LEVEL_LABEL } from '~/organizations/shared/constants';
import { MOCK_PATHS, MOCK_USERS_FORMATTED } from '../mock_data';
Vue.use(VueApollo);
jest.mock('~/alert');
describe('UsersView', () => {
let wrapper;
let mockApollo;
const successfulResponseHandler = jest.fn().mockResolvedValue(organizationUserUpdateResponse);
const mockToastShow = jest.fn();
const createComponent = ({ propsData = {}, handler = successfulResponseHandler } = {}) => {
mockApollo = createMockApollo([[organizationUserUpdateMutation, handler]]);
wrapper = mountExtended(UsersView, {
const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
wrapper = mountFn(UsersView, {
propsData: {
loading: false,
users: MOCK_USERS_FORMATTED,
pageInfo: pageInfoMultiplePages,
...propsData,
...props,
},
provide: {
paths: MOCK_PATHS,
},
apolloProvider: mockApollo,
mocks: {
$toast: {
show: mockToastShow,
},
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
const findGlLoading = () => wrapper.findComponent(GlLoadingIcon);
const findUsersTable = () => wrapper.findComponent(UsersTable);
const findGlButton = () => wrapper.findComponent(GlButton);
const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination);
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const listboxSelectOwner = () => findListbox().vm.$emit('select', 'OWNER');
afterEach(() => {
mockApollo = null;
});
const findUserDetailsDrawer = () => wrapper.findComponent(UserDetailsDrawer);
const findUsersTable = () => wrapper.findComponent(UsersTable);
describe.each`
description | loading | usersData
......@@ -67,7 +37,7 @@ describe('UsersView', () => {
${'when not loading and has no users'} | ${false} | ${[]}
`('$description', ({ loading, usersData }) => {
beforeEach(() => {
createComponent({ propsData: { loading, users: usersData } });
createComponent({ props: { loading, users: usersData } });
});
it(`does ${loading ? '' : 'not '}render loading icon`, () => {
......@@ -102,142 +72,49 @@ describe('UsersView', () => {
});
describe('Organization role', () => {
it('renders listbox with role options', () => {
createComponent();
const mockUser = MOCK_USERS_FORMATTED[0];
expect(wrapper.findComponent(GlCollapsibleListbox).props()).toMatchObject({
items: [
{
text: 'User',
value: 'DEFAULT',
},
{
text: 'Owner',
value: 'OWNER',
},
],
selected: MOCK_USERS_FORMATTED[0].accessLevel.stringValue,
disabled: false,
});
beforeEach(() => {
createComponent({ mountFn: mount });
});
it('does not render tooltip', () => {
createComponent();
const tooltipContainer = findListbox().element.parentNode;
const tooltip = getBinding(tooltipContainer, 'gl-tooltip');
it("render an organization role button with the user's role", () => {
const userAccessLevel = mockUser.accessLevel.stringValue;
expect(tooltip.value.disabled).toBe(true);
expect(tooltipContainer.getAttribute('tabindex')).toBe(null);
expect(findGlButton().text()).toBe(ACCESS_LEVEL_LABEL[userAccessLevel]);
});
describe('when user is last owner of organization', () => {
const [firstUser] = MOCK_USERS_FORMATTED;
beforeEach(() => {
createComponent({
propsData: {
loading: false,
users: [{ ...firstUser, isLastOwner: true }],
},
});
describe('when the organization role button is clicked', () => {
beforeEach(async () => {
await findGlButton().trigger('click');
});
it('renders listbox as disabled', () => {
expect(findListbox().props('disabled')).toBe(true);
it("sets the user details drawer's active user to selected user", () => {
expect(findUserDetailsDrawer().props('user')).toBe(mockUser);
});
it('renders tooltip and makes element focusable', () => {
const tooltipContainer = findListbox().element.parentNode;
const tooltip = getBinding(tooltipContainer, 'gl-tooltip');
describe('when the user details drawer is closed', () => {
it("reset the user details drawer's active user to null", async () => {
await findUserDetailsDrawer().vm.$emit('close');
expect(tooltip.value).toEqual({
title: 'Organizations must have at least one owner.',
disabled: false,
expect(findGlButton().props('user')).toBeUndefined();
});
expect(tooltipContainer.getAttribute('tabindex')).toBe('0');
});
});
describe('when role is changed', () => {
afterEach(async () => {
// clean up any unresolved GraphQL mutations
await waitForPromises();
});
describe('when the user details drawer is loading', () => {
it('disable the organization role button', async () => {
await findUserDetailsDrawer().vm.$emit('loading', true);
it('calls GraphQL mutation with correct variables', () => {
createComponent();
listboxSelectOwner();
expect(successfulResponseHandler).toHaveBeenCalledWith({
input: {
id: MOCK_USERS_FORMATTED[0].gid,
accessLevel: 'OWNER',
},
});
expect(findGlButton().props('disabled')).toBe(true);
});
});
it('shows dropdown as loading while waiting for GraphQL mutation', async () => {
createComponent();
listboxSelectOwner();
await nextTick();
expect(findListbox().props('loading')).toBe(true);
});
it('shows toast when GraphQL mutation is successful', async () => {
createComponent();
listboxSelectOwner();
await waitForPromises();
expect(mockToastShow).toHaveBeenCalledWith('Organization role was updated successfully.');
});
it('emits role-change event when GraphQL mutation is successful', async () => {
createComponent();
listboxSelectOwner();
await waitForPromises();
expect(wrapper.emitted('role-change')).toEqual([[]]);
});
it('calls createAlert when GraphQL mutation has validation error', async () => {
const errorResponseHandler = jest
.fn()
.mockResolvedValue(organizationUserUpdateResponseWithErrors);
createComponent({
handler: errorResponseHandler,
});
listboxSelectOwner();
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: 'You cannot change the access of the last owner from the organization',
});
});
it('calls createAlert when GraphQL mutation has network error', async () => {
const error = new Error();
const errorResponseHandler = jest.fn().mockRejectedValue(error);
createComponent({
handler: errorResponseHandler,
});
listboxSelectOwner();
await waitForPromises();
describe('when the user role has been changed', () => {
it('emits role-change event', async () => {
await findUserDetailsDrawer().vm.$emit('role-change');
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred updating the organization role. Please try again.',
error,
captureError: true,
});
expect(wrapper.emitted('role-change')).toHaveLength(1);
});
});
});
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册