feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
这个提交包含在:
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* OmniCard AI (CRMmp) - mpuniapp 名片主 NFC 场景模式切换测试套件 (TDD Red -> Green)
|
||||
*
|
||||
* 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 pages/owner/nfc/mode 设计编写
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MODE_PAGE = path.resolve(
|
||||
__dirname,
|
||||
'../../../mpuniapp/pages/owner/nfc/mode.uvue'
|
||||
);
|
||||
const MODE_UTS = path.resolve(
|
||||
__dirname,
|
||||
'../../../mpuniapp/pages/owner/nfc/useMode.uts'
|
||||
);
|
||||
|
||||
type NFCSceneMode = 'business' | 'social';
|
||||
type NFCStatus = 'active' | 'suspended' | 'unbound';
|
||||
type NFCMaterial = 'wood' | 'metal' | 'pvc';
|
||||
|
||||
type NFCRecord = {
|
||||
id: string | number;
|
||||
uid: string;
|
||||
tenantId: string | number;
|
||||
cardId?: string | number | null;
|
||||
batchNo: string;
|
||||
material: NFCMaterial;
|
||||
status: NFCStatus;
|
||||
sceneMode: NFCSceneMode;
|
||||
lastScannedAt?: string | Date | null;
|
||||
};
|
||||
|
||||
type ApplySceneParams = {
|
||||
uid: string;
|
||||
cardId: string | number;
|
||||
sceneMode: string;
|
||||
device: NFCRecord | null;
|
||||
};
|
||||
|
||||
type ApplySceneApi = {
|
||||
switchNfcCard: (params: { uid: string; cardId: number }) => Promise<NFCRecord>;
|
||||
updateNfcDevice: (payload: NFCRecord & { id: string | number }) => Promise<NFCRecord>;
|
||||
};
|
||||
|
||||
type ApplySceneResult = {
|
||||
ok: boolean;
|
||||
error: string;
|
||||
device: NFCRecord | null;
|
||||
};
|
||||
|
||||
type UseModeModule = {
|
||||
parseSceneMode: (raw: string | null | undefined) => NFCSceneMode | null;
|
||||
applyScene: (
|
||||
params: ApplySceneParams,
|
||||
api?: ApplySceneApi
|
||||
) => Promise<ApplySceneResult>;
|
||||
};
|
||||
|
||||
function loadUseMode(): UseModeModule {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require(MODE_UTS) as UseModeModule;
|
||||
}
|
||||
|
||||
function makeDevice(overrides: Partial<NFCRecord> = {}): NFCRecord {
|
||||
return {
|
||||
id: 88,
|
||||
uid: 'UID-NFC-8801',
|
||||
tenantId: 1,
|
||||
cardId: 101,
|
||||
batchNo: 'B2026-01',
|
||||
material: 'pvc',
|
||||
status: 'active',
|
||||
sceneMode: 'business',
|
||||
lastScannedAt: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('OmniCard AI 名片主 NFC 场景模式切换 (useMode.uts)', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
(global as { uni?: Record<string, unknown> }).uni = {
|
||||
getLocale: () => 'zh-CN',
|
||||
showToast: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete (global as { uni?: Record<string, unknown> }).uni;
|
||||
});
|
||||
|
||||
describe('1. parseSceneMode 只放行 business | social', () => {
|
||||
it('1.1 合法值原样返回', () => {
|
||||
const { parseSceneMode } = loadUseMode();
|
||||
|
||||
expect(parseSceneMode('business')).toBe('business');
|
||||
expect(parseSceneMode('social')).toBe('social');
|
||||
});
|
||||
|
||||
it('1.2 非法值、空值、大小写变体一律拒绝', () => {
|
||||
const { parseSceneMode } = loadUseMode();
|
||||
|
||||
expect(parseSceneMode('')).toBeNull();
|
||||
expect(parseSceneMode('party')).toBeNull();
|
||||
expect(parseSceneMode('BUSINESS')).toBeNull();
|
||||
expect(parseSceneMode('Social')).toBeNull();
|
||||
expect(parseSceneMode(null)).toBeNull();
|
||||
expect(parseSceneMode(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. applyScene 换绑与写 sceneMode', () => {
|
||||
it('2.1 cardId 不同且设备 active 时先 switch 再 update', async () => {
|
||||
const { applyScene } = loadUseMode();
|
||||
const device = makeDevice({ cardId: 101, status: 'active' });
|
||||
const calls: string[] = [];
|
||||
|
||||
const switched = makeDevice({
|
||||
cardId: 202,
|
||||
status: 'active',
|
||||
sceneMode: 'business',
|
||||
});
|
||||
const updated = makeDevice({
|
||||
cardId: 202,
|
||||
status: 'active',
|
||||
sceneMode: 'social',
|
||||
});
|
||||
|
||||
const api: ApplySceneApi = {
|
||||
switchNfcCard: jest.fn(async (params: { uid: string; cardId: number }) => {
|
||||
calls.push('switch');
|
||||
expect(params.uid).toBe('UID-NFC-8801');
|
||||
expect(params.cardId).toBe(202);
|
||||
return switched;
|
||||
}),
|
||||
updateNfcDevice: jest.fn(
|
||||
async (payload: NFCRecord & { id: string | number }) => {
|
||||
calls.push('update');
|
||||
expect(payload.id).toBe(88);
|
||||
expect(payload.sceneMode).toBe('social');
|
||||
return updated;
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
const result = await applyScene(
|
||||
{
|
||||
uid: 'UID-NFC-8801',
|
||||
cardId: 202,
|
||||
sceneMode: 'social',
|
||||
device,
|
||||
},
|
||||
api
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.error).toBe('');
|
||||
expect(result.device?.sceneMode).toBe('social');
|
||||
expect(result.device?.cardId).toBe(202);
|
||||
expect(calls).toEqual(['switch', 'update']);
|
||||
expect(api.switchNfcCard).toHaveBeenCalledTimes(1);
|
||||
expect(api.updateNfcDevice).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('2.2 同一 cardId 只 update,不 switch', async () => {
|
||||
const { applyScene } = loadUseMode();
|
||||
const device = makeDevice({ cardId: 101, status: 'active' });
|
||||
const updated = makeDevice({ cardId: 101, sceneMode: 'social' });
|
||||
|
||||
const api: ApplySceneApi = {
|
||||
switchNfcCard: jest.fn(async () => device),
|
||||
updateNfcDevice: jest.fn(async () => updated),
|
||||
};
|
||||
|
||||
const result = await applyScene(
|
||||
{
|
||||
uid: 'UID-NFC-8801',
|
||||
cardId: '101',
|
||||
sceneMode: 'social',
|
||||
device,
|
||||
},
|
||||
api
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(api.switchNfcCard).not.toHaveBeenCalled();
|
||||
expect(api.updateNfcDevice).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('2.3 suspended 设备拒绝切换且不调用任何 API', async () => {
|
||||
const { applyScene } = loadUseMode();
|
||||
const device = makeDevice({ status: 'suspended', cardId: 101 });
|
||||
|
||||
const api: ApplySceneApi = {
|
||||
switchNfcCard: jest.fn(async () => device),
|
||||
updateNfcDevice: jest.fn(async () => device),
|
||||
};
|
||||
|
||||
const result = await applyScene(
|
||||
{
|
||||
uid: 'UID-NFC-8801',
|
||||
cardId: 202,
|
||||
sceneMode: 'social',
|
||||
device,
|
||||
},
|
||||
api
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error.length).toBeGreaterThan(0);
|
||||
expect(result.error).toMatch(/挂失|暂停|suspended/i);
|
||||
expect(api.switchNfcCard).not.toHaveBeenCalled();
|
||||
expect(api.updateNfcDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OmniCard AI NFC 场景切换页存在 (mode.uvue)', () => {
|
||||
it('3.1 页面文件存在,且为 uts 单页与单一主 CTA', () => {
|
||||
expect(fs.existsSync(MODE_PAGE)).toBe(true);
|
||||
const source = fs.readFileSync(MODE_PAGE, 'utf8');
|
||||
expect(source.includes('script setup lang="uts"')).toBe(true);
|
||||
expect(source.includes('getOmniText')).toBe(true);
|
||||
expect(source.includes('应用场景') || source.includes("t('apply')")).toBe(
|
||||
true
|
||||
);
|
||||
expect(source.includes('#F7F8FA')).toBe(true);
|
||||
expect(source.includes('#0B5CFF')).toBe(true);
|
||||
expect((source.match(/primary-cta-btn/g) || []).length).toBeGreaterThan(0);
|
||||
expect(source.includes('v-model="state.uid"')).toBe(true);
|
||||
expect(source.includes('v-model="state.cardId"')).toBe(true);
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户