Files
gochat/frontend/app/javascript/dashboard/components-next/message/bubbles/specs/Activity.spec.js
T
Rogeeandrogee 35727be51a HH-560: localize upstream activity messages (#137)
* fix(HH-560): localize upstream activity messages

* fix(HH-560): preserve unknown activity content

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-23 22:11:00 +08:00

360 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('');
});
});