文件
OmniCard/backend/test/mpuniapp/nfc-scan.test.ts
wikiboss eec259a3a7 feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换
- 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片
- 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转
- 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出
- 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志
- 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票
- 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏
- 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
2026-09-20 23:47:47 +08:00

342 行
11 KiB
TypeScript

/**
* OmniCard AI (CRMmp) - 名片主 NFC 贴卡激活测试套件 (TDD Red -> Green)
*
* 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 PRD pages/owner/nfc/scan 设计编写
*/
import * as fs from 'fs';
import * as path from 'path';
const SCAN_PAGE = path.resolve(__dirname, '../../../mpuniapp/pages/owner/nfc/scan.uvue');
const SCAN_UTS = path.resolve(__dirname, '../../../mpuniapp/pages/owner/nfc/useScan.uts');
type NFCRecordFixture = {
id: string | number;
uid: string;
tenantId: string | number;
cardId?: string | number | null;
batchNo: string;
material: 'wood' | 'metal' | 'pvc';
status: 'active' | 'suspended' | 'unbound';
sceneMode: 'business' | 'social';
};
type ActivateByTapInput = {
uid: string;
cardId?: string | number | null;
deviceCardId?: string | number | null;
};
type ScanState = {
uid: string;
cardIdRaw: string;
deviceCardId: number | null;
submitting: boolean;
scanning: boolean;
manualInput: boolean;
errorMessage: string;
empty: boolean;
record: NFCRecordFixture | null;
};
type UseScanModule = {
parseUid: (raw: string | number | null | undefined) => string;
parseCardId: (raw: string | number | null | undefined) => number | null;
activateByTap: (
input: ActivateByTapInput,
existing?: { cardId?: string | number | null } | null
) => Promise<NFCRecordFixture>;
startScan: (state: ScanState) => boolean;
createScanState: () => ScanState;
};
type UniToastOption = {
title: string;
icon?: string;
};
type UniRequestSuccessRes = {
statusCode: number;
data: unknown;
header: Record<string, string>;
};
type UniRequestOption = {
url: string;
method?: string;
data?: Record<string, unknown>;
success?: (res: UniRequestSuccessRes) => void;
fail?: (err: { errMsg: string }) => void;
complete?: () => void;
};
const EMOJI_RE = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/;
function buildNfcRecord(overrides?: Partial<NFCRecordFixture>): NFCRecordFixture {
return {
id: 41,
uid: '04AABBCCDD',
tenantId: 8,
cardId: 12,
batchNo: 'B202609',
material: 'pvc',
status: 'active',
sceneMode: 'business',
...overrides,
};
}
describe('OmniCard AI 名片主 NFC 贴卡激活业务逻辑测试 (useScan.uts)', () => {
let storageMap: Map<string, string>;
let toastCalls: UniToastOption[];
let lastRequestOptions: UniRequestOption | null;
let requestCallCount: number;
let mockRequestResponse: {
statusCode?: number;
data?: {
code?: number;
message?: string;
data?: unknown;
};
failError?: { errMsg: string };
};
beforeEach(() => {
storageMap = new Map();
toastCalls = [];
lastRequestOptions = null;
requestCallCount = 0;
mockRequestResponse = {
statusCode: 200,
data: {
code: 1000,
message: 'success',
data: buildNfcRecord(),
},
};
jest.resetModules();
(global as { uni?: Record<string, unknown> }).uni = {
getStorageSync: (key: string) => storageMap.get(key) || '',
setStorageSync: (key: string, val: string) => storageMap.set(key, val),
removeStorageSync: (key: string) => storageMap.delete(key),
showToast: (options: UniToastOption) => {
toastCalls.push(options);
},
reLaunch: jest.fn(),
getLocale: () => 'zh-CN',
request: (options: UniRequestOption) => {
requestCallCount += 1;
lastRequestOptions = options;
setTimeout(() => {
if (mockRequestResponse.failError) {
options.fail?.(mockRequestResponse.failError);
} else {
options.success?.({
statusCode: mockRequestOptionsStatus(),
data: mockRequestResponse.data,
header: {},
});
}
options.complete?.();
}, 0);
return {
abort: jest.fn(),
};
},
};
});
afterEach(() => {
delete (global as { uni?: Record<string, unknown> }).uni;
});
function mockRequestOptionsStatus(): number {
return mockRequestResponse.statusCode != null ? mockRequestResponse.statusCode : 200;
}
function loadUseScan(): UseScanModule {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('../../../mpuniapp/pages/owner/nfc/useScan.uts') as UseScanModule;
}
describe('1. parseUid 去空且非空才合法', () => {
it('1.1 空白与空串解析为空,视为非法', () => {
const { parseUid } = loadUseScan();
expect(parseUid('')).toBe('');
expect(parseUid(' ')).toBe('');
expect(parseUid(null)).toBe('');
expect(parseUid(undefined)).toBe('');
});
it('1.2 两端空白被去掉,非空结果合法', () => {
const { parseUid } = loadUseScan();
expect(parseUid(' 04:AA:BB:CC ')).toBe('04:AA:BB:CC');
expect(parseUid('04AABBCCDD')).toBe('04AABBCCDD');
expect(parseUid(4012)).toBe('4012');
});
});
describe('2. parseCardId 仅正整数合法', () => {
it('2.1 空值、零、负数、小数、非数字均为非法', () => {
const { parseCardId } = loadUseScan();
expect(parseCardId('')).toBeNull();
expect(parseCardId(' ')).toBeNull();
expect(parseCardId(null)).toBeNull();
expect(parseCardId(undefined)).toBeNull();
expect(parseCardId(0)).toBeNull();
expect(parseCardId('0')).toBeNull();
expect(parseCardId(-1)).toBeNull();
expect(parseCardId('-8')).toBeNull();
expect(parseCardId(1.5)).toBeNull();
expect(parseCardId('12.3')).toBeNull();
expect(parseCardId('abc')).toBeNull();
});
it('2.2 正整数字符串与数字解析为 number', () => {
const { parseCardId } = loadUseScan();
expect(parseCardId(12)).toBe(12);
expect(parseCardId('12')).toBe(12);
expect(parseCardId(' 8 ')).toBe(8);
});
});
describe('3. activateByTap 调用 bind 或 activate', () => {
it('3.1 入参 cardId 合法时调用 bindNfcDevice', async () => {
const { activateByTap } = loadUseScan();
const record = await activateByTap({ uid: ' 04AABBCCDD ', cardId: '12' });
expect(requestCallCount).toBe(1);
expect(lastRequestOptions).not.toBeNull();
expect(lastRequestOptions?.method).toBe('POST');
expect(lastRequestOptions?.url).toContain('/admin/nfc/bind/bind');
expect(lastRequestOptions?.data?.uid).toBe('04AABBCCDD');
expect(lastRequestOptions?.data?.cardId).toBe(12);
expect(record.uid).toBe('04AABBCCDD');
expect(record.cardId).toBe(12);
expect(record.status).toBe('active');
});
it('3.2 设备已有 cardId 且入参 cardId 空时调用 activateNfcDevice', async () => {
const { activateByTap } = loadUseScan();
mockRequestResponse.data = {
code: 1000,
message: 'success',
data: buildNfcRecord({ cardId: 88 }),
};
const record = await activateByTap(
{ uid: '04AABBCCDD', cardId: '' },
{ cardId: 88 }
);
expect(requestCallCount).toBe(1);
expect(lastRequestOptions).not.toBeNull();
expect(lastRequestOptions?.method).toBe('POST');
expect(lastRequestOptions?.url).toContain('/admin/nfc/bind/activate');
expect(lastRequestOptions?.data?.uid).toBe('04AABBCCDD');
expect(lastRequestOptions?.data?.cardId).toBeUndefined();
expect(record.cardId).toBe(88);
});
it('3.3 入参带 deviceCardId 且 cardId 空时走 activate', async () => {
const { activateByTap } = loadUseScan();
await activateByTap({ uid: '04AABBCCDD', cardId: null, deviceCardId: 5 });
expect(requestCallCount).toBe(1);
expect(lastRequestOptions?.url).toContain('/admin/nfc/bind/activate');
});
it('3.4 空 UID 不发请求并抛出错误信息', async () => {
const { activateByTap } = loadUseScan();
await expect(activateByTap({ uid: ' ', cardId: 12 })).rejects.toThrow();
expect(requestCallCount).toBe(0);
});
it('3.5 无入参 cardId 且设备亦无 cardId 时不发请求并抛错', async () => {
const { activateByTap } = loadUseScan();
await expect(activateByTap({ uid: '04AABBCCDD', cardId: '' })).rejects.toThrow();
expect(requestCallCount).toBe(0);
});
it('3.6 接口失败时抛出服务端错误信息', async () => {
const { activateByTap } = loadUseScan();
mockRequestResponse.data = {
code: 1001,
message: '设备未在台账中',
data: null,
};
await expect(activateByTap({ uid: '04AABBCCDD', cardId: 12 })).rejects.toThrow(
'设备未在台账中'
);
});
});
describe('4. startScan 不伪造成功', () => {
it('4.1 调不到原生 NFC 时回退手动输入且不发激活请求', () => {
const { createScanState, startScan } = loadUseScan();
const state = createScanState();
const ok = startScan(state);
expect(ok).toBe(false);
expect(state.manualInput).toBe(true);
expect(state.scanning).toBe(false);
expect(state.record).toBeNull();
expect(requestCallCount).toBe(0);
});
});
});
describe('OmniCard AI 名片主 NFC 贴卡激活页契约 (scan.uvue)', () => {
it('页面存在且引用 bindNfcDevice 或 activateByTap', () => {
expect(fs.existsSync(SCAN_PAGE)).toBe(true);
expect(fs.existsSync(SCAN_UTS)).toBe(true);
const pageSource = fs.readFileSync(SCAN_PAGE, 'utf8');
const logicSource = fs.readFileSync(SCAN_UTS, 'utf8');
const combined = `${pageSource}\n${logicSource}`;
expect(combined.includes('bindNfcDevice') || combined.includes('activateByTap')).toBe(
true
);
expect(pageSource.includes('script setup lang="uts"')).toBe(true);
expect(pageSource.includes('<view')).toBe(true);
expect(pageSource.includes('<text')).toBe(true);
expect(pageSource.includes('<input')).toBe(true);
expect(pageSource.includes('<button')).toBe(true);
expect(pageSource.includes('getOmniText')).toBe(true);
expect(pageSource.includes('#0B5CFF')).toBe(true);
expect(pageSource.includes('#F7F8FA')).toBe(true);
expect(pageSource.includes('#101828')).toBe(true);
expect(pageSource.includes('#667085')).toBe(true);
expect(pageSource.includes('#E3E7ED')).toBe(true);
});
it('无 emoji、无 any,模板无硬编码中文主文案', () => {
const pageSource = fs.readFileSync(SCAN_PAGE, 'utf8');
const logicSource = fs.readFileSync(SCAN_UTS, 'utf8');
const testSource = fs.readFileSync(__filename, 'utf8');
const template = pageSource.split('<script')[0];
expect(EMOJI_RE.test(pageSource)).toBe(false);
expect(EMOJI_RE.test(logicSource)).toBe(false);
expect(EMOJI_RE.test(testSource)).toBe(false);
expect(pageSource.includes(': any') || pageSource.includes(' as any')).toBe(false);
expect(logicSource.includes(': any') || logicSource.includes(' as any')).toBe(false);
expect(/[\u4e00-\u9fff]/.test(template)).toBe(false);
expect(pageSource.includes('innerAudioContext') || pageSource.includes('autoplay')).toBe(
false
);
});
});