文件
OmniCard/backend/test/mpuniapp/nfc-mode.test.ts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

317 行
9.5 KiB
TypeScript

/**
* 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 OfflineWriteResult = {
ok: boolean;
error: string;
};
type UseModeModule = {
parseSceneMode: (raw: string | null | undefined) => NFCSceneMode | null;
applyScene: (
params: ApplySceneParams,
api?: ApplySceneApi
) => Promise<ApplySceneResult>;
canWriteOfflineVCard: () => boolean;
writeOfflineVCard: (payload?: {
uid?: string;
cardId?: string | number;
vcard?: string;
}) => Promise<OfflineWriteResult>;
resolveModeText: (key: string) => string;
};
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('3. 离线 vCard 烧录无真实写芯片时必须失败', () => {
it('3.1 canWriteOfflineVCard 为 false,无 NDEF 写入能力', () => {
const { canWriteOfflineVCard } = loadUseMode();
expect(canWriteOfflineVCard()).toBe(false);
});
it('3.2 writeOfflineVCard 不得返回成功,error 走 resolve 中英文案', async () => {
const { writeOfflineVCard, resolveModeText } = loadUseMode();
const uniBag = (global as { uni?: Record<string, unknown> }).uni as {
showToast: jest.Mock;
getLocale: () => string;
};
const result = await writeOfflineVCard({
uid: 'UID-NFC-8801',
cardId: 101,
vcard: 'BEGIN:VCARD\nVERSION:3.0\nEND:VCARD',
});
expect(result.ok).toBe(false);
expect(result.error).toBe(resolveModeText('offlineUnavailable'));
expect(result.error).toMatch(/暂不能写入离线名片/);
uniBag.getLocale = () => 'en-US';
const enResult = await writeOfflineVCard({ uid: 'UID-NFC-8801' });
expect(enResult.ok).toBe(false);
expect(enResult.error).toMatch(/cannot write offline/i);
const toastTitles = uniBag.showToast.mock.calls.map(
(c: unknown[]) => (c[0] as { title?: string }).title || ''
);
expect(
toastTitles.some((t: string) =>
/已烧录|burned|written|success/i.test(t)
)
).toBe(false);
});
it('3.3 resolveModeText(offlineUnavailable) 中英均有自然文案', () => {
const { resolveModeText } = loadUseMode();
const uniBag = (global as { uni?: Record<string, unknown> }).uni as {
getLocale: () => string;
};
uniBag.getLocale = () => 'zh-CN';
expect(resolveModeText('offlineUnavailable')).toBe(
'此设备暂不能写入离线名片'
);
uniBag.getLocale = () => 'en-US';
expect(resolveModeText('offlineUnavailable')).toMatch(
/cannot write offline/i
);
});
});
});
describe('OmniCard AI NFC 场景切换页存在 (mode.uvue)', () => {
it('4.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);
});
it('4.2 页面展示离线不可写说明,且不伪造烧录成功', () => {
const source = fs.readFileSync(MODE_PAGE, 'utf8');
expect(
source.includes("offlineUnavailable") ||
source.includes('此设备暂不能写入离线名片')
).toBe(true);
expect(source.includes('mode-offline-hint')).toBe(true);
expect(source.includes('已烧录')).toBe(false);
expect(/NDEF[\s\S]{0,40}success|writeNdef|假写入/i.test(source)).toBe(
false
);
});
});