补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
317 行
9.5 KiB
TypeScript
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
|
|
);
|
|
});
|
|
});
|