/** * OmniCard AI (CRMmp) - mpuniapp 统一登录骨架测试套件 (TDD Red -> Green) * * 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 PRD pages/common/login 设计编写 */ type UniToastOption = { title: string; icon?: string; }; type UniRequestSuccessRes = { statusCode: number; data: unknown; header: Record; }; type UniRequestOption = { url: string; method?: string; data?: Record; success?: (res: UniRequestSuccessRes) => void; fail?: (err: { errMsg: string }) => void; complete?: () => void; }; type LoginState = { tab: 'password' | 'phone' | 'email'; username: string; password: string; countryCode: string; phone: string; smsCode: string; email: string; submitting: boolean; errorMessage: string; countdown: number; }; type UseLoginModule = { createLoginState: () => LoginState; completeLogin: (token: string, tenantId?: string) => boolean; submitPasswordLogin: (state: LoginState) => Promise; submitPhoneLogin: (state: LoginState) => boolean; sendMagicLink: (state: LoginState) => boolean; sendSmsCode: (state: LoginState) => boolean; tapSocialLogin: (provider: 'google' | 'apple') => void; resolveLoginText: (key: string) => string; tickCountdown: (state: LoginState) => void; fillTestAccount: (state: LoginState) => void; }; describe('OmniCard AI 用户端统一登录业务逻辑测试 (useLogin.uts)', () => { let storageMap: Map; let toastCalls: UniToastOption[]; let lastRequestOptions: UniRequestOption | null; let requestCallCount: number; let navigateBackCount: number; let switchTabCount: number; beforeEach(() => { storageMap = new Map(); toastCalls = []; lastRequestOptions = null; requestCallCount = 0; navigateBackCount = 0; switchTabCount = 0; jest.resetModules(); (global as { uni?: Record }).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(), switchTab: (_options: { url: string }) => { switchTabCount += 1; }, navigateBack: () => { navigateBackCount += 1; }, getLocale: () => 'zh-CN', request: (options: UniRequestOption) => { requestCallCount += 1; lastRequestOptions = options; setTimeout(() => { options.success?.({ statusCode: 200, data: { code: 1000, message: 'success', data: {} }, header: {}, }); options.complete?.(); }, 0); return { abort: jest.fn(), }; }, }; }); afterEach(() => { delete (global as { uni?: Record }).uni; }); function loadUseLogin(): UseLoginModule { // eslint-disable-next-line @typescript-eslint/no-require-imports return require('../../../mpuniapp/pages/common/useLogin.uts') as UseLoginModule; } describe('1. completeLogin 写入本地凭证', () => { it('1.1 completeLogin 会 setStorage token 并跳转名片页', () => { const { completeLogin } = loadUseLogin(); const ok = completeLogin('mock_jwt_login_t09'); expect(ok).toBe(true); expect(storageMap.get('token')).toBe('mock_jwt_login_t09'); expect(switchTabCount).toBe(1); }); it('1.2 传入 tenantId 时同步写入 tenant_id', () => { const { completeLogin } = loadUseLogin(); completeLogin('mock_jwt_login_t09', 'tenant_omni_88'); expect(storageMap.get('token')).toBe('mock_jwt_login_t09'); expect(storageMap.get('tenant_id')).toBe('tenant_omni_88'); expect(switchTabCount).toBe(1); }); it('1.3 未传 tenantId 时不写入 tenant_id', () => { const { completeLogin } = loadUseLogin(); completeLogin('mock_jwt_only_token'); expect(storageMap.get('token')).toBe('mock_jwt_only_token'); expect(storageMap.has('tenant_id')).toBe(false); expect(switchTabCount).toBe(1); }); }); describe('2. 空手机号 / 空邮箱不提交', () => { it('2.1 空手机号不提交验证码登录', () => { const { createLoginState, submitPhoneLogin } = loadUseLogin(); const state = createLoginState(); state.smsCode = '123456'; const ok = submitPhoneLogin(state); expect(ok).toBe(false); expect(storageMap.has('token')).toBe(false); expect(requestCallCount).toBe(0); expect(lastRequestOptions).toBeNull(); expect(toastCalls.length).toBeGreaterThan(0); expect(toastCalls[0].title).toContain('手机号'); }); it('2.2 空邮箱不提交 Magic Link', () => { const { createLoginState, sendMagicLink } = loadUseLogin(); const state = createLoginState(); state.tab = 'email'; const ok = sendMagicLink(state); expect(ok).toBe(false); expect(storageMap.has('token')).toBe(false); expect(requestCallCount).toBe(0); expect(lastRequestOptions).toBeNull(); expect(toastCalls.length).toBeGreaterThan(0); expect(toastCalls[0].title).toContain('邮箱'); }); }); describe('3. 社交登录不伪造成功', () => { it('3.1 Google / Apple 仅 toast 即将开放且不写 token', () => { const { tapSocialLogin } = loadUseLogin(); tapSocialLogin('google'); tapSocialLogin('apple'); expect(storageMap.has('token')).toBe(false); expect(navigateBackCount).toBe(0); expect(requestCallCount).toBe(0); expect(toastCalls.length).toBe(2); expect(toastCalls[0].title).toContain('即将开放'); expect(toastCalls[1].title).toContain('即将开放'); }); }); describe('4. 极简品牌感文案与国际化解析', () => { it('4.1 正确解析品牌感文案,杜绝空字符串与硬编码', () => { const { resolveLoginText } = loadUseLogin(); expect(resolveLoginText('brandTag').length).toBeGreaterThan(0); expect(resolveLoginText('subtitle').length).toBeGreaterThan(0); expect(resolveLoginText('orContinueWith').length).toBeGreaterThan(0); expect(resolveLoginText('termsNotice').length).toBeGreaterThan(0); expect(resolveLoginText('termsOfService').length).toBeGreaterThan(0); expect(resolveLoginText('privacyPolicy').length).toBeGreaterThan(0); }); }); describe('5. 验证码倒计时机制', () => { it('5.1 手机号合法时发送验证码触发 60s 倒计时', () => { const { createLoginState, sendSmsCode, tickCountdown } = loadUseLogin(); const state = createLoginState(); state.phone = '13800138000'; sendSmsCode(state); expect(state.countdown).toBe(60); // 递减一次 tickCountdown(state); expect(state.countdown).toBe(59); // 倒计时中不能重复触发 const again = sendSmsCode(state); expect(again).toBe(false); expect(state.countdown).toBe(59); }); }); describe('6. 账号密码真实登录与前后端对齐', () => { it('6.1 空账号或空密码不提交', async () => { const { createLoginState, submitPasswordLogin } = loadUseLogin(); const state = createLoginState(); state.username = ''; state.password = ''; const ok = await submitPasswordLogin(state); expect(ok).toBe(false); expect(storageMap.has('token')).toBe(false); expect(requestCallCount).toBe(0); expect(toastCalls.length).toBeGreaterThan(0); expect(toastCalls[0].title).toContain('账号'); // 仅有账号没有密码 state.username = 'admin'; state.password = ''; const ok2 = await submitPasswordLogin(state); expect(ok2).toBe(false); expect(storageMap.has('token')).toBe(false); expect(toastCalls[toastCalls.length - 1].title).toContain('密码'); }); it('6.2 账号密码登录成功:对齐 /admin/base/open/login 接口契约,写入 token 并跳转', async () => { const { createLoginState, submitPasswordLogin } = loadUseLogin(); const state = createLoginState(); state.username = 'admin'; state.password = '123456'; // 模拟 uni.request 响应 Cool-Admin 成功契约 (global as { uni?: Record }).uni!.request = (options: UniRequestOption) => { requestCallCount += 1; lastRequestOptions = options; setTimeout(() => { options.success?.({ statusCode: 200, data: { code: 1000, message: 'success', data: { token: 'mock_admin_jwt_token_2026', expire: 7200, refreshToken: 'mock_refresh_token', }, }, header: {}, }); options.complete?.(); }, 0); return { abort: jest.fn() }; }; const ok = await submitPasswordLogin(state); expect(ok).toBe(true); expect(storageMap.get('token')).toBe('mock_admin_jwt_token_2026'); expect(switchTabCount).toBe(1); expect(lastRequestOptions?.url).toContain('/admin/base/open/login'); expect(lastRequestOptions?.data).toEqual({ username: 'admin', password: '123456', }); }); it('6.3 账号密码登录失败:后端返回 1001 时正确提示错误并置错误信息', async () => { const { createLoginState, submitPasswordLogin } = loadUseLogin(); const state = createLoginState(); state.username = 'admin'; state.password = 'wrong_password'; (global as { uni?: Record }).uni!.request = (options: UniRequestOption) => { requestCallCount += 1; lastRequestOptions = options; setTimeout(() => { options.success?.({ statusCode: 200, data: { code: 1001, message: '账户或密码不正确~', }, header: {}, }); options.complete?.(); }, 0); return { abort: jest.fn() }; }; const ok = await submitPasswordLogin(state); expect(ok).toBe(false); expect(storageMap.has('token')).toBe(false); expect(switchTabCount).toBe(0); expect(state.errorMessage).toBe('账户或密码不正确~'); expect(toastCalls[toastCalls.length - 1].title).toBe('账户或密码不正确~'); }); it('6.4 一键填入测试账号便于快速验证', () => { const { createLoginState, fillTestAccount } = loadUseLogin(); const state = createLoginState(); expect(state.username).toBe(''); expect(state.password).toBe(''); fillTestAccount(state); expect(state.username).toBe('admin'); expect(state.password).toBe('123456'); }); }); });