fix: 修复客服权限与收件箱配置问题
This commit is contained in:
@@ -13,6 +13,17 @@ describe('#routeIsAccessibleFor', () => {
|
||||
expect(routeIsAccessibleFor(route, ['agent'])).toEqual(false);
|
||||
expect(routeIsAccessibleFor(route, ['administrator'])).toEqual(true);
|
||||
});
|
||||
|
||||
it('allows agents to open the help center entry route', () => {
|
||||
const route = {
|
||||
name: 'portals_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(routeIsAccessibleFor(route, ['agent'])).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#defaultRedirectPage', () => {
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"RESET_DESC": "Copy this new password now and share it with the agent securely. The old password no longer works, and this password cannot be shown again after closing.",
|
||||
"COPY": "Copy password",
|
||||
"COPY_SUCCESS": "Password copied to clipboard",
|
||||
"COPY_FAILED": "Unable to copy the password. Please select and copy it manually.",
|
||||
"DONE": "I have saved it"
|
||||
},
|
||||
"SEARCH_PLACEHOLDER": "Search agents...",
|
||||
|
||||
@@ -393,6 +393,12 @@
|
||||
"USERNAME": "Login username",
|
||||
"PASSWORD": "Login password",
|
||||
"DESIRED_PRESENCE": "Initial presence",
|
||||
"PRESENCE": {
|
||||
"ONLINE": "Online",
|
||||
"BUSY": "Busy",
|
||||
"AWAY": "Away",
|
||||
"OFFLINE": "Offline"
|
||||
},
|
||||
"WEBHOOK_URL": "Connector webhook URL",
|
||||
"SUBMIT_BUTTON": "Create Shangwutong channel",
|
||||
"API": {
|
||||
@@ -1180,6 +1186,45 @@
|
||||
"REFRESH": "Refresh status",
|
||||
"DESIRED_PRESENCE": "Desired presence",
|
||||
"DESIRED_PRESENCE_HELP": "Changes are applied online. Offline logs out only this Shangwutong account.",
|
||||
"PRESENCE": {
|
||||
"ONLINE": "Online",
|
||||
"BUSY": "Busy",
|
||||
"AWAY": "Away",
|
||||
"OFFLINE": "Offline"
|
||||
},
|
||||
"STATUS_FIELDS": {
|
||||
"CONNECTION_STATUS": "Connection status",
|
||||
"ACTUAL_PRESENCE": "Current presence",
|
||||
"CREDENTIAL_STATUS": "Credential status",
|
||||
"LAST_HEARTBEAT_AT": "Last heartbeat"
|
||||
},
|
||||
"STATUS_VALUES": {
|
||||
"UNKNOWN": "Unknown",
|
||||
"CONNECTION": {
|
||||
"PENDING": "Pending",
|
||||
"LOGGING_IN": "Logging in",
|
||||
"CONNECTED": "Connected",
|
||||
"DEGRADED": "Degraded",
|
||||
"RELOGIN_REQUIRED": "Relogin required",
|
||||
"VERIFICATION_REQUIRED": "Verification required",
|
||||
"AUTH_FAILED": "Authentication failed",
|
||||
"DISABLED": "Disabled",
|
||||
"OFFLINE": "Offline"
|
||||
},
|
||||
"PRESENCE": {
|
||||
"ONLINE": "Online",
|
||||
"BUSY": "Busy",
|
||||
"AWAY": "Away",
|
||||
"OFFLINE": "Offline"
|
||||
},
|
||||
"CREDENTIAL": {
|
||||
"PENDING": "Pending verification",
|
||||
"VERIFYING": "Verifying",
|
||||
"APPLIED": "Applied",
|
||||
"REJECTED": "Rejected",
|
||||
"VERIFICATION_REQUIRED": "Verification required"
|
||||
}
|
||||
},
|
||||
"PASSWORD": "Replace password",
|
||||
"PASSWORD_HELP": "Leave blank to keep the current password. A new password is verified online before replacing the working credential.",
|
||||
"WEBHOOK_URL": "Connector webhook URL",
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"RESET_DESC": "请立即复制并安全地将此新密码交给客服。旧密码已失效,关闭后将无法再次查看。",
|
||||
"COPY": "复制密码",
|
||||
"COPY_SUCCESS": "密码已复制到剪贴板",
|
||||
"COPY_FAILED": "无法复制密码,请手动选择并复制。",
|
||||
"DONE": "我已保存"
|
||||
},
|
||||
"SEARCH_PLACEHOLDER": "搜索客服代表...",
|
||||
|
||||
@@ -393,6 +393,12 @@
|
||||
"USERNAME": "登录账号",
|
||||
"PASSWORD": "登录密码",
|
||||
"DESIRED_PRESENCE": "初始在线状态",
|
||||
"PRESENCE": {
|
||||
"ONLINE": "在线",
|
||||
"BUSY": "忙碌",
|
||||
"AWAY": "离开",
|
||||
"OFFLINE": "离线"
|
||||
},
|
||||
"WEBHOOK_URL": "Connector Webhook 地址",
|
||||
"SUBMIT_BUTTON": "创建商务通渠道",
|
||||
"API": {
|
||||
@@ -838,50 +844,50 @@
|
||||
},
|
||||
"ASSIGNMENT": {
|
||||
"TITLE": "对话分配",
|
||||
"DESCRIPTION": "Automatically assign incoming conversations to available agents based on assignment policies",
|
||||
"ENABLE_AUTO_ASSIGNMENT": "Enable automatic conversation assignment",
|
||||
"DEFAULT_RULES_TITLE": "Default assignment rules",
|
||||
"DEFAULT_RULES_DESCRIPTION": "Using the default assignment behavior for all conversations",
|
||||
"DEFAULT_RULE_1": "Earliest created conversations first",
|
||||
"DEFAULT_RULE_2": "Round robin distribution",
|
||||
"CUSTOMIZE_WITH_POLICY": "Customize with assignment policy",
|
||||
"USING_POLICY": "Using custom assignment policy for this inbox",
|
||||
"CUSTOMIZE_POLICY": "Customize with assignment policy",
|
||||
"DELETE_POLICY": "Delete policy",
|
||||
"POLICY_LABEL": "Assignment policy",
|
||||
"ASSIGNMENT_ORDER_LABEL": "Assignment Order",
|
||||
"ASSIGNMENT_METHOD_LABEL": "Assignment Method",
|
||||
"DESCRIPTION": "根据分配策略将新对话自动分配给可用客服",
|
||||
"ENABLE_AUTO_ASSIGNMENT": "启用自动分配对话",
|
||||
"DEFAULT_RULES_TITLE": "默认分配规则",
|
||||
"DEFAULT_RULES_DESCRIPTION": "所有对话使用默认分配方式",
|
||||
"DEFAULT_RULE_1": "优先分配最早创建的对话",
|
||||
"DEFAULT_RULE_2": "轮询分配",
|
||||
"CUSTOMIZE_WITH_POLICY": "使用分配策略自定义",
|
||||
"USING_POLICY": "此收件箱正在使用自定义分配策略",
|
||||
"CUSTOMIZE_POLICY": "自定义分配策略",
|
||||
"DELETE_POLICY": "删除策略",
|
||||
"POLICY_LABEL": "分配策略",
|
||||
"ASSIGNMENT_ORDER_LABEL": "分配顺序",
|
||||
"ASSIGNMENT_METHOD_LABEL": "分配方式",
|
||||
"POLICY_STATUS": {
|
||||
"ACTIVE": "状态",
|
||||
"INACTIVE": "Inactive"
|
||||
"ACTIVE": "启用",
|
||||
"INACTIVE": "停用"
|
||||
},
|
||||
"PRIORITY": {
|
||||
"EARLIEST_CREATED": "Earliest created",
|
||||
"LONGEST_WAITING": "Longest waiting"
|
||||
"EARLIEST_CREATED": "最早创建",
|
||||
"LONGEST_WAITING": "等待时间最长"
|
||||
},
|
||||
"METHOD": {
|
||||
"ROUND_ROBIN": "Round robin",
|
||||
"BALANCED": "Balanced assignment"
|
||||
"ROUND_ROBIN": "轮询分配",
|
||||
"BALANCED": "均衡分配"
|
||||
},
|
||||
"UPGRADE_PROMPT": "Custom assignment policies are available on the Business plan",
|
||||
"UPGRADE_TO_BUSINESS": "Upgrade to Business",
|
||||
"DEFAULT_POLICY_LINKED": "Default policy linked",
|
||||
"DEFAULT_POLICY_DESCRIPTION": "Link a custom assignment policy to customize how conversations are assigned to agents in this inbox.",
|
||||
"LINK_EXISTING_POLICY": "Link existing policy",
|
||||
"CREATE_NEW_POLICY": "Create new policy",
|
||||
"NO_POLICIES": "No assignment policies found",
|
||||
"VIEW_ALL_POLICIES": "View all policies",
|
||||
"CURRENT_BEHAVIOR": "Currently using default assignment behavior:",
|
||||
"LINK_SUCCESS": "Assignment policy linked successfully",
|
||||
"LINK_ERROR": "Failed to link assignment policy"
|
||||
"UPGRADE_PROMPT": "自定义分配策略适用于商业版套餐",
|
||||
"UPGRADE_TO_BUSINESS": "升级到商业版",
|
||||
"DEFAULT_POLICY_LINKED": "已关联默认策略",
|
||||
"DEFAULT_POLICY_DESCRIPTION": "关联自定义分配策略,以配置此收件箱如何将对话分配给客服。",
|
||||
"LINK_EXISTING_POLICY": "关联现有策略",
|
||||
"CREATE_NEW_POLICY": "创建新策略",
|
||||
"NO_POLICIES": "未找到分配策略",
|
||||
"VIEW_ALL_POLICIES": "查看全部策略",
|
||||
"CURRENT_BEHAVIOR": "当前使用默认分配方式:",
|
||||
"LINK_SUCCESS": "分配策略关联成功",
|
||||
"LINK_ERROR": "分配策略关联失败"
|
||||
},
|
||||
"ASSIGNMENT_POLICY": {
|
||||
"DELETE_CONFIRM_TITLE": "Delete assignment policy?",
|
||||
"DELETE_CONFIRM_MESSAGE": "Are you sure you want to remove this assignment policy from this inbox? The inbox will revert to default assignment rules.",
|
||||
"DELETE_CONFIRM_TITLE": "删除分配策略?",
|
||||
"DELETE_CONFIRM_MESSAGE": "确定要从此收件箱移除该分配策略吗?移除后,收件箱将恢复使用默认分配规则。",
|
||||
"CANCEL": "取消",
|
||||
"CONFIRM_DELETE": "删除",
|
||||
"DELETE_SUCCESS": "Assignment policy removed successfully",
|
||||
"DELETE_ERROR": "Failed to remove assignment policy"
|
||||
"DELETE_SUCCESS": "分配策略已移除",
|
||||
"DELETE_ERROR": "移除分配策略失败"
|
||||
},
|
||||
"FACEBOOK_REAUTHORIZE": {
|
||||
"TITLE": "重新授权",
|
||||
@@ -1175,15 +1181,54 @@
|
||||
}
|
||||
},
|
||||
"SHANGWUTONG_SETTINGS": {
|
||||
"RUNTIME_STATUS": "Connector 运行状态",
|
||||
"RUNTIME_STATUS_HELP": "显示 Connector 最近回报的连接、在线与凭据状态。",
|
||||
"RUNTIME_STATUS": "连接器运行状态",
|
||||
"RUNTIME_STATUS_HELP": "显示连接器最近回报的连接、在线与凭据状态。",
|
||||
"REFRESH": "刷新状态",
|
||||
"DESIRED_PRESENCE": "期望在线状态",
|
||||
"DESIRED_PRESENCE_HELP": "状态在线切换;选择 offline 只会退出当前商务通账号。",
|
||||
"DESIRED_PRESENCE_HELP": "状态在线切换;选择离线只会退出当前商务通账号。",
|
||||
"PRESENCE": {
|
||||
"ONLINE": "在线",
|
||||
"BUSY": "忙碌",
|
||||
"AWAY": "离开",
|
||||
"OFFLINE": "离线"
|
||||
},
|
||||
"STATUS_FIELDS": {
|
||||
"CONNECTION_STATUS": "连接状态",
|
||||
"ACTUAL_PRESENCE": "当前在线状态",
|
||||
"CREDENTIAL_STATUS": "凭据状态",
|
||||
"LAST_HEARTBEAT_AT": "最后心跳时间"
|
||||
},
|
||||
"STATUS_VALUES": {
|
||||
"UNKNOWN": "未知",
|
||||
"CONNECTION": {
|
||||
"PENDING": "等待连接",
|
||||
"LOGGING_IN": "正在登录",
|
||||
"CONNECTED": "已连接",
|
||||
"DEGRADED": "连接异常",
|
||||
"RELOGIN_REQUIRED": "需要重新登录",
|
||||
"VERIFICATION_REQUIRED": "需要验证",
|
||||
"AUTH_FAILED": "认证失败",
|
||||
"DISABLED": "已禁用",
|
||||
"OFFLINE": "已离线"
|
||||
},
|
||||
"PRESENCE": {
|
||||
"ONLINE": "在线",
|
||||
"BUSY": "忙碌",
|
||||
"AWAY": "离开",
|
||||
"OFFLINE": "离线"
|
||||
},
|
||||
"CREDENTIAL": {
|
||||
"PENDING": "等待验证",
|
||||
"VERIFYING": "正在验证",
|
||||
"APPLIED": "已生效",
|
||||
"REJECTED": "验证失败",
|
||||
"VERIFICATION_REQUIRED": "需要验证"
|
||||
}
|
||||
},
|
||||
"PASSWORD": "修改密码",
|
||||
"PASSWORD_HELP": "留空表示保持原密码。新密码在线验证成功后才替换当前可用凭据。",
|
||||
"WEBHOOK_URL": "Connector Webhook 地址",
|
||||
"WEBHOOK_URL_HELP": "所有启用的商务通收件箱必须使用同一个 Connector 地址。"
|
||||
"WEBHOOK_URL": "连接器 Webhook 地址",
|
||||
"WEBHOOK_URL_HELP": "所有启用的商务通收件箱必须使用同一个连接器地址。"
|
||||
},
|
||||
"CHANNELS": {
|
||||
"MESSENGER": "Messenger",
|
||||
|
||||
@@ -322,7 +322,7 @@
|
||||
"CAPTAIN_INBOXES": "收件箱",
|
||||
"CAPTAIN_SETTINGS": "设置",
|
||||
"HOME": "首页",
|
||||
"AGENTS": "客服代理",
|
||||
"AGENTS": "客服",
|
||||
"AGENT_BOTS": "机器人",
|
||||
"AUDIT_LOGS": "审计日志",
|
||||
"INBOXES": "收件箱",
|
||||
|
||||
@@ -102,10 +102,7 @@ const portalRoutes = [
|
||||
{
|
||||
path: getPortalRoute(':navigationPath'),
|
||||
name: 'portals_index',
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.HELP_CENTER,
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
meta,
|
||||
component: PortalsIndex,
|
||||
},
|
||||
];
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import helpcenterRoutes from './helpcenter.routes';
|
||||
|
||||
const findRoute = name => {
|
||||
const [rootRoute] = helpcenterRoutes.routes;
|
||||
return rootRoute.children.find(route => route.name === name);
|
||||
};
|
||||
|
||||
describe('Help center routes', () => {
|
||||
it('allows agents to enter the help center router', () => {
|
||||
expect(findRoute('portals_index').meta.permissions).toContain('agent');
|
||||
});
|
||||
|
||||
it('keeps article routes accessible to agents', () => {
|
||||
expect(findRoute('portals_articles_index').meta.permissions).toContain(
|
||||
'agent'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps portal creation restricted to administrators or custom roles', () => {
|
||||
expect(findRoute('portals_new').meta.permissions).not.toContain('agent');
|
||||
});
|
||||
});
|
||||
+6
-2
@@ -29,8 +29,12 @@ const open = () => dialogRef.value?.open();
|
||||
const close = () => dialogRef.value?.close();
|
||||
|
||||
const copyPassword = async password => {
|
||||
await copyTextToClipboard(password);
|
||||
useAlert(t('AGENT_MGMT.PASSWORD_DIALOG.COPY_SUCCESS'));
|
||||
try {
|
||||
await copyTextToClipboard(password);
|
||||
useAlert(t('AGENT_MGMT.PASSWORD_DIALOG.COPY_SUCCESS'));
|
||||
} catch {
|
||||
useAlert(t('AGENT_MGMT.PASSWORD_DIALOG.COPY_FAILED'));
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ open, close });
|
||||
|
||||
+12
-4
@@ -143,10 +143,18 @@ export default {
|
||||
<label>
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.DESIRED_PRESENCE') }}
|
||||
<select v-model="desiredPresence">
|
||||
<option value="online">online</option>
|
||||
<option value="busy">busy</option>
|
||||
<option value="away">away</option>
|
||||
<option value="offline">offline</option>
|
||||
<option value="online">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.ONLINE') }}
|
||||
</option>
|
||||
<option value="busy">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.BUSY') }}
|
||||
</option>
|
||||
<option value="away">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.AWAY') }}
|
||||
</option>
|
||||
<option value="offline">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.OFFLINE') }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
|
||||
+54
-17
@@ -32,6 +32,13 @@ export default {
|
||||
this.fetchHealth();
|
||||
},
|
||||
methods: {
|
||||
statusLabel(type, value, fallback) {
|
||||
const status = value || fallback;
|
||||
const key = `INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_VALUES.${type}.${status.toUpperCase()}`;
|
||||
return this.$te(key)
|
||||
? this.$t(key)
|
||||
: this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_VALUES.UNKNOWN');
|
||||
},
|
||||
async fetchHealth() {
|
||||
this.isLoadingHealth = true;
|
||||
try {
|
||||
@@ -88,43 +95,65 @@ export default {
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-lg border border-n-weak p-3">
|
||||
<p class="text-label-small text-n-slate-10">
|
||||
connection_status
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.CONNECTION_STATUS'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p class="text-body-main text-n-slate-12">
|
||||
{{
|
||||
health?.connection_status ||
|
||||
inbox.connection_status ||
|
||||
'pending'
|
||||
statusLabel(
|
||||
'CONNECTION',
|
||||
health?.connection_status || inbox.connection_status,
|
||||
'pending'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-n-weak p-3">
|
||||
<p class="text-label-small text-n-slate-10">
|
||||
actual_presence
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.ACTUAL_PRESENCE'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p class="text-body-main text-n-slate-12">
|
||||
{{
|
||||
health?.actual_presence ||
|
||||
inbox.actual_presence ||
|
||||
'offline'
|
||||
statusLabel(
|
||||
'PRESENCE',
|
||||
health?.actual_presence || inbox.actual_presence,
|
||||
'offline'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-n-weak p-3">
|
||||
<p class="text-label-small text-n-slate-10">
|
||||
credential_status
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.CREDENTIAL_STATUS'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p class="text-body-main text-n-slate-12">
|
||||
{{
|
||||
health?.credential_status ||
|
||||
inbox.credential_status ||
|
||||
'pending'
|
||||
statusLabel(
|
||||
'CREDENTIAL',
|
||||
health?.credential_status || inbox.credential_status,
|
||||
'pending'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-n-weak p-3">
|
||||
<p class="text-label-small text-n-slate-10">
|
||||
last_heartbeat_at
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.LAST_HEARTBEAT_AT'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p class="text-body-main text-n-slate-12">
|
||||
{{
|
||||
@@ -156,10 +185,18 @@ export default {
|
||||
"
|
||||
>
|
||||
<select v-model="desiredPresence" class="w-full">
|
||||
<option value="online">online</option>
|
||||
<option value="busy">busy</option>
|
||||
<option value="away">away</option>
|
||||
<option value="offline">offline</option>
|
||||
<option value="online">
|
||||
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.ONLINE') }}
|
||||
</option>
|
||||
<option value="busy">
|
||||
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.BUSY') }}
|
||||
</option>
|
||||
<option value="away">
|
||||
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.AWAY') }}
|
||||
</option>
|
||||
<option value="offline">
|
||||
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.OFFLINE') }}
|
||||
</option>
|
||||
</select>
|
||||
</SettingsFieldSection>
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export const copyTextToClipboard = async data => {
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
try {
|
||||
textarea.focus({ preventScroll: true });
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, textarea.value.length);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { copyTextToClipboard, handleOtpPaste } from '../clipboard';
|
||||
|
||||
const mockWriteText = vi.fn();
|
||||
const mockExecCommand = vi.fn();
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
|
||||
const setClipboard = clipboard => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
@@ -148,9 +149,16 @@ describe('copyTextToClipboard', () => {
|
||||
|
||||
it('falls back when clipboard API is not available', async () => {
|
||||
setClipboard(undefined);
|
||||
const focus = vi.fn();
|
||||
vi.spyOn(document, 'createElement').mockImplementation(tagName => {
|
||||
const element = originalCreateElement(tagName);
|
||||
if (tagName === 'textarea') element.focus = focus;
|
||||
return element;
|
||||
});
|
||||
|
||||
await copyTextToClipboard('test');
|
||||
|
||||
expect(focus).toHaveBeenCalledWith({ preventScroll: true });
|
||||
expect(mockExecCommand).toHaveBeenCalledWith('copy');
|
||||
expect(document.querySelector('textarea')).toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user