244 行
9.3 KiB
TypeScript
244 行
9.3 KiB
TypeScript
/**
|
|
* 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<string, string>;
|
|
|
|
beforeEach(() => {
|
|
storageMap = new Map<string, string>();
|
|
(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<string, { code: string; name: string; dir: string }>;
|
|
};
|
|
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);
|
|
});
|
|
});
|
|
});
|