* fix(HH-560): localize upstream activity messages * fix(HH-560): preserve unknown activity content --------- Co-authored-by: Rogee <rogee@ipao.vip>
360 lines
11 KiB
JavaScript
360 lines
11 KiB
JavaScript
import { config, mount } from '@vue/test-utils';
|
||
import {
|
||
afterAll,
|
||
beforeAll,
|
||
beforeEach,
|
||
describe,
|
||
expect,
|
||
it,
|
||
vi,
|
||
} from 'vitest';
|
||
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||
|
||
import Activity from '../Activity.vue';
|
||
|
||
const mocks = vi.hoisted(() => ({
|
||
content: '',
|
||
contentAttributes: {},
|
||
}));
|
||
|
||
vi.mock('shared/helpers/timeHelper', () => ({
|
||
messageTimestamp: () => '8月6日 下午 3:12',
|
||
}));
|
||
|
||
const longURL =
|
||
'https://ada.baidu.com/site/wjzt6ah8/agent?imid=e159aa97cd2c403f2fba76899e47615f&trinfo=XzFWIaclcWbzcMY&word=%E4%B8%89%E9%98%B3%E7%97%85%E6%AF%92%E6%90%BA%E5%B8%A6';
|
||
|
||
const i18n = config.global.plugins.find(plugin => plugin.global?.locale);
|
||
const initialLocale = i18n.global.locale.value;
|
||
|
||
beforeAll(() => {
|
||
i18n.global.locale.value = 'zh_CN';
|
||
});
|
||
|
||
afterAll(() => {
|
||
i18n.global.locale.value = initialLocale;
|
||
});
|
||
|
||
vi.mock('../../provider.js', async () => {
|
||
const { ref } = await vi.importActual('vue');
|
||
return {
|
||
useMessageContext: () => ({
|
||
content: ref(mocks.content),
|
||
contentAttributes: ref(mocks.contentAttributes),
|
||
createdAt: ref(1786018000),
|
||
}),
|
||
};
|
||
});
|
||
|
||
const mountActivity = () =>
|
||
mount(Activity, {
|
||
global: {
|
||
plugins: [VueDOMPurifyHTML],
|
||
stubs: {
|
||
BaseBubble: {
|
||
template: '<div><slot /></div>',
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
describe('Activity bubble', () => {
|
||
beforeEach(() => {
|
||
i18n.global.locale.value = 'zh_CN';
|
||
mocks.content = '';
|
||
mocks.contentAttributes = {};
|
||
});
|
||
|
||
it('allows long links in system messages to wrap inside the bubble', () => {
|
||
mocks.content = `来源页面:<a href="${longURL}">${longURL}</a>`;
|
||
const wrapper = mountActivity();
|
||
|
||
expect(wrapper.get('[data-bubble-name="activity"]').classes()).toContain(
|
||
'max-w-full'
|
||
);
|
||
|
||
const content = wrapper.get('span');
|
||
expect(content.classes()).toContain('min-w-0');
|
||
expect(content.classes()).toContain('max-w-full');
|
||
expect(content.classes()).toContain('[overflow-wrap:anywhere]');
|
||
expect(content.get('a').text()).toBe(longURL);
|
||
});
|
||
|
||
it.each([
|
||
[
|
||
'Conversation was resolved by Anonymous Visitor',
|
||
'会话已由 匿名访客 解决',
|
||
],
|
||
[
|
||
'Conversation was marked resolved by Alice',
|
||
'会话已由 Alice 标记为已解决',
|
||
],
|
||
['Conversation was reopened by Alice', '会话已由 Alice 重新打开'],
|
||
[
|
||
'Conversation was marked as pending by Alice',
|
||
'会话已由 Alice 标记为待处理',
|
||
],
|
||
['Conversation was snoozed by Alice', '会话已由 Alice 推迟'],
|
||
[
|
||
'Conversation was marked resolved by system due to 2 hours of inactivity',
|
||
'会话因 2 小时无活动已由系统自动解决',
|
||
],
|
||
[
|
||
'Alice changed the priority from low to high',
|
||
'Alice 将优先级从 低 更改为 高',
|
||
],
|
||
['Assigned to Bob by Alice', 'Alice 将会话分配给 Bob'],
|
||
[
|
||
'Assigned to Bob via Support by Alice',
|
||
'Alice 通过团队 Support 将会话分配给 Bob',
|
||
],
|
||
['Alice added vip, billing', 'Alice 添加了标签:vip, billing'],
|
||
['Alice removed SLA policy Gold', 'Alice 移除了 SLA 策略 Gold'],
|
||
['Alice has muted the conversation', 'Alice 已将会话静音'],
|
||
[
|
||
'Conversation was reopened due to agent bot webhook failure',
|
||
'由于客服机器人 Webhook 失败,会话已重新打开',
|
||
],
|
||
[
|
||
'CSAT survey not sent due to outgoing message restrictions',
|
||
'由于发送消息受限,未发送 CSAT 调查',
|
||
],
|
||
[
|
||
'Auto-resolve message not sent due to outgoing message restrictions',
|
||
'由于发送消息受限,未发送自动解决消息',
|
||
],
|
||
[
|
||
'Linear issue ENG-1 was created by Alice',
|
||
'Linear 问题 ENG-1 已由 Alice 创建',
|
||
],
|
||
[
|
||
'Conversation was marked resolved by Captain due to inactivity',
|
||
'Captain 因无活动将会话标记为已解决',
|
||
],
|
||
['Sent a call permission request to Bob.', '已向 Bob 发送通话权限请求。'],
|
||
])('localizes known legacy activity content: %s', (content, expected) => {
|
||
mocks.content = content;
|
||
const wrapper = mountActivity();
|
||
|
||
expect(wrapper.get('[data-bubble-name="activity"] span').text()).toBe(
|
||
expected
|
||
);
|
||
});
|
||
|
||
it.each([
|
||
[
|
||
{ type: 'status.resolved', userName: 'Alice' },
|
||
'会话已由 Alice 标记为已解决',
|
||
],
|
||
[
|
||
{ type: 'status.contact_resolved', contactName: 'visitor' },
|
||
'会话已由 匿名访客 解决',
|
||
],
|
||
[{ type: 'status.open', userName: 'Alice' }, '会话已由 Alice 重新打开'],
|
||
[
|
||
{ type: 'status.pending', userName: 'Alice' },
|
||
'会话已由 Alice 标记为待处理',
|
||
],
|
||
[{ type: 'status.snoozed', userName: 'Alice' }, '会话已由 Alice 推迟'],
|
||
[
|
||
{ type: 'status.auto_resolved_days', count: 2 },
|
||
'会话因 2 天无活动已由系统自动解决',
|
||
],
|
||
[
|
||
{ type: 'status.auto_resolved_hours', count: 3 },
|
||
'会话因 3 小时无活动已由系统自动解决',
|
||
],
|
||
[
|
||
{ type: 'status.auto_resolved_minutes', count: 15 },
|
||
'会话因 15 分钟无活动已由系统自动解决',
|
||
],
|
||
[
|
||
{ type: 'status.system_auto_open' },
|
||
'收到新的传入消息后,系统重新打开了会话。',
|
||
],
|
||
[
|
||
{ type: 'priority.added', userName: 'Alice', newPriority: 'urgent' },
|
||
'Alice 将优先级设为 紧急的',
|
||
],
|
||
[
|
||
{
|
||
type: 'priority.updated',
|
||
userName: 'Alice',
|
||
oldPriority: 'low',
|
||
newPriority: 'high',
|
||
},
|
||
'Alice 将优先级从 低 更改为 高',
|
||
],
|
||
[{ type: 'priority.removed', userName: 'Alice' }, 'Alice 移除了优先级'],
|
||
[
|
||
{ type: 'assignee.self_assigned', userName: 'Alice' },
|
||
'Alice 将此会话分配给自己',
|
||
],
|
||
[
|
||
{ type: 'assignee.assigned', userName: 'Alice', assigneeName: 'Bob' },
|
||
'Alice 将会话分配给 Bob',
|
||
],
|
||
[{ type: 'assignee.removed', userName: 'Alice' }, 'Alice 取消了会话分配'],
|
||
[
|
||
{ type: 'team.assigned', userName: 'Alice', teamName: 'Support' },
|
||
'Alice 将会话分配给团队 Support',
|
||
],
|
||
[
|
||
{
|
||
type: 'team.assigned_with_assignee',
|
||
userName: 'Alice',
|
||
teamName: 'Support',
|
||
assigneeName: 'Bob',
|
||
},
|
||
'Alice 通过团队 Support 将会话分配给 Bob',
|
||
],
|
||
[
|
||
{ type: 'team.removed', userName: 'Alice', teamName: 'Support' },
|
||
'Alice 将会话从团队 Support 中移除',
|
||
],
|
||
[
|
||
{ type: 'labels.added', userName: 'Alice', labels: ['vip', 'billing'] },
|
||
'Alice 添加了标签:vip, billing',
|
||
],
|
||
[
|
||
{ type: 'labels.removed', userName: 'Alice', labels: 'vip' },
|
||
'Alice 移除了标签:vip',
|
||
],
|
||
[
|
||
{ type: 'sla.added', userName: 'Alice', slaName: 'Gold' },
|
||
'Alice 添加了 SLA 策略 Gold',
|
||
],
|
||
[
|
||
{ type: 'sla.removed', userName: 'Alice', slaName: 'Gold' },
|
||
'Alice 移除了 SLA 策略 Gold',
|
||
],
|
||
[{ type: 'muted', userName: 'Alice' }, 'Alice 已将会话静音'],
|
||
[{ type: 'unmuted', userName: 'Alice' }, 'Alice 已取消会话静音'],
|
||
[
|
||
{ type: 'captain.resolved', userName: 'Captain' },
|
||
'Captain 因无活动将会话标记为已解决',
|
||
],
|
||
[
|
||
{
|
||
type: 'captain.resolved_with_reason',
|
||
userName: 'Captain',
|
||
reason: 'Done',
|
||
},
|
||
'Captain 将会话标记为已解决(Done)',
|
||
],
|
||
[
|
||
{ type: 'captain.resolved_by_tool', userName: 'Captain', reason: 'Done' },
|
||
'Captain 将会话标记为已解决:Done',
|
||
],
|
||
[{ type: 'captain.open', userName: 'Captain' }, 'Captain 将会话标记为打开'],
|
||
[
|
||
{
|
||
type: 'captain.open_with_reason',
|
||
userName: 'Captain',
|
||
reason: 'Reply',
|
||
},
|
||
'Captain 将会话标记为打开(Reply)',
|
||
],
|
||
[
|
||
{ type: 'captain.auto_opened_after_agent_reply' },
|
||
'客服回复后,会话已自动标记为打开',
|
||
],
|
||
[
|
||
{ type: 'agent_bot.error_moved_to_open' },
|
||
'由于客服机器人 Webhook 失败,会话已重新打开',
|
||
],
|
||
[
|
||
{ type: 'csat.not_sent_due_to_messaging_window' },
|
||
'由于发送消息受限,未发送 CSAT 调查',
|
||
],
|
||
[
|
||
{ type: 'auto_resolve.not_sent_due_to_messaging_window' },
|
||
'由于发送消息受限,未发送自动解决消息',
|
||
],
|
||
[
|
||
{ type: 'auto_resolution_message' },
|
||
'会话已有一段时间没有活动,现将自动解决。如需进一步帮助,请发起新会话。',
|
||
],
|
||
[
|
||
{ type: 'linear.issue_created', issueId: 'ENG-1', userName: 'Alice' },
|
||
'Linear 问题 ENG-1 已由 Alice 创建',
|
||
],
|
||
[
|
||
{ type: 'linear.issue_linked', issueId: 'ENG-1', userName: 'Alice' },
|
||
'Linear 问题 ENG-1 已由 Alice 关联',
|
||
],
|
||
[
|
||
{ type: 'linear.issue_unlinked', issueId: 'ENG-1', userName: 'Alice' },
|
||
'Linear 问题 ENG-1 已由 Alice 取消关联',
|
||
],
|
||
[
|
||
{ type: 'whatsapp_call.permission_requested', contactName: 'Bob' },
|
||
'已向 Bob 发送通话权限请求。',
|
||
],
|
||
[
|
||
{ type: 'whatsapp_call.permission_granted', contactName: 'Bob' },
|
||
'Bob 已接受通话权限请求。',
|
||
],
|
||
])('localizes a structured activity type', (activity, expected) => {
|
||
mocks.content = 'Untranslated fallback';
|
||
mocks.contentAttributes = { activity };
|
||
const wrapper = mountActivity();
|
||
|
||
expect(wrapper.get('[data-bubble-name="activity"] span').text()).toBe(
|
||
expected
|
||
);
|
||
});
|
||
|
||
it.each([
|
||
['custom.unknown', 'Conversation was resolved by Alice'],
|
||
['sla.updated', 'Alice added SLA policy Gold'],
|
||
['toString', 'Conversation was reopened by Alice'],
|
||
['constructor', 'Conversation was marked as pending by Alice'],
|
||
])(
|
||
'falls back to stored content for unsupported structured type %s',
|
||
(type, content) => {
|
||
mocks.content = content;
|
||
mocks.contentAttributes = { activity: { type } };
|
||
|
||
expect(mountActivity().get('span').text()).toBe(content);
|
||
}
|
||
);
|
||
|
||
it('uses the English activity catalog when the dashboard locale is English', () => {
|
||
i18n.global.locale.value = 'en';
|
||
mocks.contentAttributes = {
|
||
activity: {
|
||
type: 'priority.updated',
|
||
userName: 'Automation System',
|
||
oldPriority: 'low',
|
||
newPriority: 'high',
|
||
},
|
||
};
|
||
|
||
expect(mountActivity().get('span').text()).toBe(
|
||
'Automation System changed the priority from Low to High'
|
||
);
|
||
});
|
||
|
||
it('sanitizes malicious dynamic activity names with the real directive', () => {
|
||
mocks.contentAttributes = {
|
||
activity: {
|
||
type: 'status.resolved',
|
||
userName: 'Alice<img src=x onerror="window.activityXss = true">',
|
||
},
|
||
};
|
||
const content = mountActivity().get('span');
|
||
|
||
expect(content.text()).toBe('会话已由 Alice 标记为已解决');
|
||
expect(content.get('img').attributes('onerror')).toBeUndefined();
|
||
expect(window.activityXss).toBeUndefined();
|
||
});
|
||
|
||
it('renders an empty activity when content is null', () => {
|
||
mocks.content = null;
|
||
const wrapper = mountActivity();
|
||
|
||
expect(wrapper.get('[data-bubble-name="activity"] span').text()).toBe('');
|
||
});
|
||
});
|