- 移除微信订阅推送功能(云函数/订阅管理页),回归纯客户端零后端架构 - 新增今日黄历默认展示卡片 TodayAlmanacCard(9 语言翻译) - i18n 修复:启用 globalInjection 修复 $t 未注入,插值消息全部改为 Messages Functions 适配小程序端(63 处) - 修复底部导航切换失效与运势页数据不刷新 - 统一设计令牌(深褐主色 + 印红强调色),组件 UI 规范重构 - 图标 iconfont 化(FontAwesome 子集化,修复 Android 豆腐块) - 新增 postcss-px2rpx 机型自适应(仅 mp-weixin 构建生效) - 新增 5 个用户旅程 E2E 测试(31 用例)与验收报告 - 补充微信小程序项目配置(appid)
135 lines
4.9 KiB
TypeScript
135 lines
4.9 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
||
import zhCN from '../../locales/zh-CN'
|
||
import zhTW from '../../locales/zh-TW'
|
||
import en from '../../locales/en'
|
||
import ja from '../../locales/ja'
|
||
import ko from '../../locales/ko'
|
||
import de from '../../locales/de'
|
||
import fr from '../../locales/fr'
|
||
import es from '../../locales/es'
|
||
import pt from '../../locales/pt'
|
||
import { i18n, I18N_OPTIONS } from '../../locales'
|
||
|
||
// 递归收集所有键路径
|
||
function collectKeys(obj: Record<string, any>, prefix = ''): string[] {
|
||
const keys: string[] = []
|
||
for (const [key, value] of Object.entries(obj)) {
|
||
const fullKey = prefix ? `${prefix}.${key}` : key
|
||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||
keys.push(...collectKeys(value, fullKey))
|
||
} else {
|
||
keys.push(fullKey)
|
||
}
|
||
}
|
||
return keys.sort()
|
||
}
|
||
|
||
// 递归收集键路径及其值,用于比较值是否为空
|
||
function collectKeysWithValues(obj: Record<string, any>, prefix = ''): Map<string, string> {
|
||
const result = new Map<string, string>()
|
||
for (const [key, value] of Object.entries(obj)) {
|
||
const fullKey = prefix ? `${prefix}.${key}` : key
|
||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||
const nested = collectKeysWithValues(value, fullKey)
|
||
nested.forEach((v, k) => result.set(k, v))
|
||
} else {
|
||
result.set(fullKey, String(value))
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
const zhCNKeys = collectKeys(zhCN)
|
||
const localeModules = {
|
||
'zh-TW': zhTW,
|
||
'en': en,
|
||
'ja': ja,
|
||
'ko': ko,
|
||
'de': de,
|
||
'fr': fr,
|
||
'es': es,
|
||
'pt': pt,
|
||
} as const
|
||
|
||
describe('TC-I18N-001~009: 国际化完整性验证', () => {
|
||
for (const [locale, module] of Object.entries(localeModules)) {
|
||
const localeName = {
|
||
'zh-TW': '繁体中文',
|
||
'en': '英文',
|
||
'ja': '日文',
|
||
'ko': '韩文',
|
||
'de': '德文',
|
||
'fr': '法文',
|
||
'es': '西班牙文',
|
||
'pt': '葡萄牙文',
|
||
}[locale]
|
||
|
||
describe(`${locale} (${localeName})`, () => {
|
||
it(`应包含所有 zh-CN 中的键`, () => {
|
||
const localeKeys = collectKeys(module)
|
||
const missingKeys = zhCNKeys.filter(k => !localeKeys.includes(k))
|
||
if (missingKeys.length > 0) {
|
||
console.warn(`[${locale}] 缺少以下键:`, missingKeys)
|
||
}
|
||
expect(missingKeys).toEqual([])
|
||
})
|
||
|
||
it(`不应包含多余的键`, () => {
|
||
const localeKeys = collectKeys(module)
|
||
const extraKeys = localeKeys.filter(k => !zhCNKeys.includes(k))
|
||
if (extraKeys.length > 0) {
|
||
console.warn(`[${locale}] 存在额外键:`, extraKeys)
|
||
}
|
||
expect(extraKeys).toEqual([])
|
||
})
|
||
|
||
it(`所有键的值不应为空字符串`, () => {
|
||
const values = collectKeysWithValues(module)
|
||
const emptyValues: string[] = []
|
||
values.forEach((value, key) => {
|
||
if (value.trim() === '') emptyValues.push(key)
|
||
})
|
||
expect(emptyValues).toEqual([])
|
||
})
|
||
|
||
it(`应包含正确数量的键 (${zhCNKeys.length} 个)`, () => {
|
||
const localeKeys = collectKeys(module)
|
||
expect(localeKeys.length).toBe(zhCNKeys.length)
|
||
})
|
||
})
|
||
}
|
||
})
|
||
|
||
describe('TC-I18N-010: i18n 配置验证(防 P0 回归)', () => {
|
||
it('createI18n 应启用 globalInjection,确保小程序端模板 $t 可用', () => {
|
||
// legacy:false 模式下 globalInjection 必须为 true,
|
||
// 否则小程序端模板渲染报 "TypeError: a.$t is not a function",全部页面异常
|
||
expect(I18N_OPTIONS.globalInjection).toBe(true)
|
||
})
|
||
|
||
it('createI18n 应使用 Composition API(legacy: false)', () => {
|
||
expect(I18N_OPTIONS.legacy).toBe(false)
|
||
expect((i18n as any).mode).toBe('composition')
|
||
})
|
||
|
||
it('app.use(i18n) 后应在 globalProperties 注入 $t(小程序端模板可用)', async () => {
|
||
const { createSSRApp } = await import('vue')
|
||
const app = createSSRApp({ render: () => null })
|
||
app.use(i18n)
|
||
// globalInjection:true 会将 $t/$rt 注入 Vue 全局属性,供模板/render 使用
|
||
const globalProperties = (app as any).config.globalProperties
|
||
expect(globalProperties).toBeDefined()
|
||
expect(typeof globalProperties.$t).toBe('function')
|
||
expect(typeof globalProperties.$rt).toBe('function')
|
||
})
|
||
|
||
it('Messages Functions 应正确插值(uni-app 小程序端不支持 {var} 字符串插值,必须用函数)', () => {
|
||
i18n.global.locale.value = 'zh-CN'
|
||
// 小程序端 {count} 字符串插值失效,函数形式是官方兼容方案
|
||
expect(i18n.global.t('search.daysUnit', { count: 30 })).toBe('30天')
|
||
expect(i18n.global.t('search.resultCount', { count: 20 })).toBe('共20条结果')
|
||
expect(i18n.global.t('fortune.overallScore', { score: 55 })).toBe('综合运势:55分')
|
||
expect(i18n.global.t('search.dateFormat', { year: 2026, month: 8, day: 18 })).toBe('2026年8月18日')
|
||
expect(i18n.global.t('search.historyConditionDays', { count: 2, days: 30 })).toBe('2个条件,30天范围')
|
||
})
|
||
}) |