文件
OmniCard/backend/test/mpuniapp/login.test.ts

224 行
6.8 KiB
TypeScript

/**
* 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<string, string>;
};
type UniRequestOption = {
url: string;
method?: string;
data?: Record<string, unknown>;
success?: (res: UniRequestSuccessRes) => void;
fail?: (err: { errMsg: string }) => void;
complete?: () => void;
};
type LoginState = {
tab: 'phone' | 'email';
countryCode: string;
phone: string;
smsCode: string;
email: string;
submitting: boolean;
errorMessage: string;
countdown: number;
};
type UseLoginModule = {
createLoginState: () => LoginState;
completeLogin: (token: string, tenantId?: string) => boolean;
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;
};
describe('OmniCard AI 用户端统一登录业务逻辑测试 (useLogin.uts)', () => {
let storageMap: Map<string, string>;
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<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(),
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<string, unknown> }).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);
});
});
});