/** * OmniCard AI (CRMmp) - 访客端多语言动态切换与 RTL 镜像契约测试 (TDD) * 严格按照 AGENTS.md 与国际化 WCAG 标准规范 */ import * as fs from 'fs'; import * as path from 'path'; const LOCALE_UTS = path.resolve(__dirname, '../../../mpuniapp/common/locale.uts'); const SELECTOR_UVUE = path.resolve( __dirname, '../../../mpuniapp/components/i18n-selector/i18n-selector.uvue' ); const PORTAL_INDEX = path.resolve(__dirname, '../../../mpuniapp/pages/portal/index.uvue'); describe('Point 04: 访客端多语言动态切换与 RTL 镜像无感适配', () => { let storageMap: Map; beforeEach(() => { storageMap = new Map(); (global as { uni?: unknown }).uni = { getStorageSync: (key: string) => storageMap.get(key) || '', setStorageSync: (key: string, val: string) => storageMap.set(key, val), getLocale: () => '', getSystemInfoSync: () => ({ language: '' }), }; jest.resetModules(); }); afterEach(() => { delete (global as { uni?: unknown }).uni; jest.resetModules(); }); describe('1. 7 种主流语言映射与解析 (resolveAppLocale / normalizeLocale)', () => { it('正确将各变体语言映射到 7 大主流标准语种', () => { const { resolveAppLocale, normalizeLocale } = require(LOCALE_UTS) as { resolveAppLocale: (raw: string, fallback?: string) => string; normalizeLocale?: (raw: string) => string; }; const resolve = resolveAppLocale || normalizeLocale; expect(typeof resolve).toBe('function'); // zh 族系 -> zh-CN expect(resolve('zh')).toBe('zh-CN'); expect(resolve('zh-CN')).toBe('zh-CN'); expect(resolve('zh-Hans')).toBe('zh-CN'); expect(resolve('zh-TW')).toBe('zh-CN'); expect(resolve('zh-HK')).toBe('zh-CN'); // en 族系 -> en-US expect(resolve('en')).toBe('en-US'); expect(resolve('en-US')).toBe('en-US'); expect(resolve('en-GB')).toBe('en-US'); // ar 族系 -> ar-SA expect(resolve('ar')).toBe('ar-SA'); expect(resolve('ar-SA')).toBe('ar-SA'); expect(resolve('ar-EG')).toBe('ar-SA'); expect(resolve('ar-AE')).toBe('ar-SA'); // es 族系 -> es-ES expect(resolve('es')).toBe('es-ES'); expect(resolve('es-ES')).toBe('es-ES'); expect(resolve('es-MX')).toBe('es-ES'); // de 族系 -> de-DE expect(resolve('de')).toBe('de-DE'); expect(resolve('de-DE')).toBe('de-DE'); expect(resolve('de-AT')).toBe('de-DE'); // fr 族系 -> fr-FR expect(resolve('fr')).toBe('fr-FR'); expect(resolve('fr-FR')).toBe('fr-FR'); expect(resolve('fr-CA')).toBe('fr-FR'); // ja 族系 -> ja-JP expect(resolve('ja')).toBe('ja-JP'); expect(resolve('ja-JP')).toBe('ja-JP'); // 未知语种回退 en-US expect(resolve('ko')).toBe('en-US'); expect(resolve('ru')).toBe('en-US'); expect(resolve('')).toBe('en-US'); expect(resolve('unknown-LANG')).toBe('en-US'); }); it('7 种主流语言在词典中均完整注册', () => { const { omniLocales } = require(LOCALE_UTS) as { omniLocales: Record; }; const expectedCodes = ['zh-CN', 'en-US', 'ar-SA', 'es-ES', 'de-DE', 'fr-FR', 'ja-JP']; for (const code of expectedCodes) { expect(omniLocales[code]).toBeDefined(); expect(omniLocales[code].code).toBe(code); expect(typeof omniLocales[code].name).toBe('string'); expect(omniLocales[code].name.length).toBeGreaterThan(0); expect(['ltr', 'rtl']).toContain(omniLocales[code].dir); } }); }); describe('2. RTL 判定逻辑 (isRtlLocale)', () => { it('对 ar-SA, he-IL, fa-IR, ur-PK 等 RTL 语种准确判定为 true', () => { const { isRtlLocale } = require(LOCALE_UTS) as { isRtlLocale: (lang: string) => boolean; }; expect(isRtlLocale('ar-SA')).toBe(true); expect(isRtlLocale('ar')).toBe(true); expect(isRtlLocale('ar-EG')).toBe(true); expect(isRtlLocale('he-IL')).toBe(true); expect(isRtlLocale('he')).toBe(true); expect(isRtlLocale('fa-IR')).toBe(true); expect(isRtlLocale('fa')).toBe(true); expect(isRtlLocale('ur-PK')).toBe(true); expect(isRtlLocale('ur')).toBe(true); }); it('对 LTR 语种准确判定为 false', () => { const { isRtlLocale } = require(LOCALE_UTS) as { isRtlLocale: (lang: string) => boolean; }; expect(isRtlLocale('zh-CN')).toBe(false); expect(isRtlLocale('en-US')).toBe(false); expect(isRtlLocale('es-ES')).toBe(false); expect(isRtlLocale('de-DE')).toBe(false); expect(isRtlLocale('fr-FR')).toBe(false); expect(isRtlLocale('ja-JP')).toBe(false); expect(isRtlLocale('')).toBe(false); expect(isRtlLocale('ko-KR')).toBe(false); }); }); describe('3. 动态切换与本地存储记忆 (setAppLocale / getCurrentLocale / appLocaleState)', () => { it('setAppLocale 能正确持久化到本地存储并同步全局状态', () => { const { setAppLocale, getCurrentLocale, appLocaleState } = require(LOCALE_UTS) as { setAppLocale: (lang: string) => void; getCurrentLocale: () => string; appLocaleState: { current: string; isRtl: boolean; dir: string }; }; setAppLocale('de-DE'); expect(getCurrentLocale()).toBe('de-DE'); expect(storageMap.get('omni_locale')).toBe('de-DE'); expect(appLocaleState.current).toBe('de-DE'); expect(appLocaleState.isRtl).toBe(false); expect(appLocaleState.dir).toBe('ltr'); // 切换至 RTL 语种 setAppLocale('ar-SA'); expect(getCurrentLocale()).toBe('ar-SA'); expect(storageMap.get('omni_locale')).toBe('ar-SA'); expect(appLocaleState.current).toBe('ar-SA'); expect(appLocaleState.isRtl).toBe(true); expect(appLocaleState.dir).toBe('rtl'); }); it('从本地存储恢复上一次记忆的语言', () => { storageMap.set('omni_locale', 'fr-FR'); const { getCurrentLocale, appLocaleState, initAppLocale } = require(LOCALE_UTS) as { getCurrentLocale: () => string; appLocaleState: { current: string; isRtl: boolean }; initAppLocale?: () => string; }; if (typeof initAppLocale === 'function') { initAppLocale(); } expect(getCurrentLocale()).toBe('fr-FR'); expect(appLocaleState.current).toBe('fr-FR'); expect(appLocaleState.isRtl).toBe(false); }); }); describe('4. 默认 GeoIP 语言回退 (resolveGeoLocale)', () => { it('根据访客的国家码准确映射主流语言或回退 en-US', () => { const { resolveGeoLocale } = require(LOCALE_UTS) as { resolveGeoLocale: (countryCode?: string, fallback?: string) => string; }; expect(resolveGeoLocale('CN')).toBe('zh-CN'); expect(resolveGeoLocale('HK')).toBe('zh-CN'); expect(resolveGeoLocale('TW')).toBe('zh-CN'); expect(resolveGeoLocale('SA')).toBe('ar-SA'); expect(resolveGeoLocale('AE')).toBe('ar-SA'); expect(resolveGeoLocale('EG')).toBe('ar-SA'); expect(resolveGeoLocale('ES')).toBe('es-ES'); expect(resolveGeoLocale('MX')).toBe('es-ES'); expect(resolveGeoLocale('DE')).toBe('de-DE'); expect(resolveGeoLocale('AT')).toBe('de-DE'); expect(resolveGeoLocale('FR')).toBe('fr-FR'); expect(resolveGeoLocale('JP')).toBe('ja-JP'); // 其余地区与空值回退 en-US expect(resolveGeoLocale('US')).toBe('en-US'); expect(resolveGeoLocale('GB')).toBe('en-US'); expect(resolveGeoLocale('SG')).toBe('en-US'); expect(resolveGeoLocale('')).toBe('en-US'); expect(resolveGeoLocale(undefined)).toBe('en-US'); }); }); describe('5. 组件与微官网 RTL 适配契约', () => { it('i18n-selector.uvue 存在且包含 7 种主流语言与触控无障碍尺寸', () => { expect(fs.existsSync(SELECTOR_UVUE)).toBe(true); const code = fs.readFileSync(SELECTOR_UVUE, 'utf8'); // 验证 7 大语种标识 expect(code.includes('zh-CN')).toBe(true); expect(code.includes('en-US')).toBe(true); expect(code.includes('ar-SA')).toBe(true); expect(code.includes('es-ES')).toBe(true); expect(code.includes('de-DE')).toBe(true); expect(code.includes('fr-FR')).toBe(true); expect(code.includes('ja-JP')).toBe(true); // 验证调用或引入 setAppLocale expect(code.includes('setAppLocale')).toBe(true); // 约束:严禁包含任何 emoji const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; expect(emojiRegex.test(code)).toBe(false); // 满足触控区 >= 44x44 expect(code.includes('44px') || code.includes('min-height: 44') || code.includes('48px')).toBe(true); }); it('index.uvue 正确引入 i18n-selector 并支持 RTL 根容器适配', () => { const code = fs.readFileSync(PORTAL_INDEX, 'utf8'); expect(code.includes('i18n-selector') || code.includes('I18nSelector')).toBe(true); expect(code.includes('direction: rtl') || code.includes('portal-rtl')).toBe(true); expect(code.includes('isRtl')).toBe(true); // 约束:严禁包含 emoji const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; expect(emojiRegex.test(code)).toBe(false); }); }); });