Files
photo-live/apps/viewer/src/__tests__/error-coverage.spec.ts
T
浅念 7385785504 feat(i18n): T15 full-stack language audit with coverage assertions
- viewer/admin 错误码→文案映射按契约 ErrorCodes 枚举全集登记(机器守护
  spec 遍历枚举断言无缺漏 + 映射键在 zh/en 词条均存在 + locale 键集
  递归相等);mobile 镜像补齐 DOWNLOAD_DISABLED(P4a 增补漏登)
- 漏译修复:viewer.photoRemoved en 侧缺失(审计测试捕获);
  DOWNLOAD_DISABLED 观众侧专属文案 + useDownload 专属提示接线;
  admin apiMsg 增 RATE_LIMITED/ALBUM_NOT_PUBLIC/DOWNLOAD_DISABLED/
  SYNC_AUTH_FAILED 四码词条
- 运行时抽查:admin 主旅程 en 版 e2e(登录→相册管理全英文 + 偏好
  持久);embed iframe lang=en 渲染与深链跟随 e2e(zh 侧既有用例覆盖)
- contracts 新增 ./errors 浏览器安全子路径(errors.ts 纯 TS);
  admin 增 @photo/contracts devDep(仅测试消费)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 23:44:27 +08:00

72 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { ErrorCodes } from '@photo/contracts/errors';
import { errorCodeI18nKeys } from '../api/errors';
import en from '../i18n/locales/en';
import zhCN from '../i18n/locales/zh-CN';
// T15 全端语言验收(P4b Task 6):错误码→文案映射的机器守护。
// 1. 契约 ErrorCodes 枚举全集在本表无缺漏(A12「主要提示无漏译」);
// 2. 表内每个文案键在 zh-CN / en 两份词条都真实存在(防键注册了但一侧
// 没写文案的真漏译;点路径解析);
// 3. zh-CN / en 两份词条键集合递归相等(模板级 zh/en 一致性,镜像 admin
// 侧 locale 守护与 P3 mobile arb 完整性测试)。
// 服务端不做文案本地化(错误体 message 仅英文诊断兜底,客户端按稳定 code
// 映射——见 openapi.yaml info.description),故本表即观众侧错误文案全集。
type Messages = Record<string, unknown>;
/** 递归收集词条文件的叶子键点路径集合(errors.albumNotFound 等)。 */
function leafPaths(messages: Messages, prefix = ''): Set<string> {
const paths = new Set<string>();
for (const [key, value] of Object.entries(messages)) {
const path = prefix === '' ? key : `${prefix}.${key}`;
if (value !== null && typeof value === 'object') {
for (const child of leafPaths(value as Messages, path)) paths.add(child);
} else {
paths.add(path);
}
}
return paths;
}
/** 按点路径取词条值(不存在返回 undefined)。 */
function resolvePath(messages: Messages, path: string): unknown {
let current: unknown = messages;
for (const segment of path.split('.')) {
if (current === null || typeof current !== 'object') return undefined;
current = (current as Messages)[segment];
}
return current;
}
describe('错误码文案覆盖(T15/A12)', () => {
const zhPaths = leafPaths(zhCN as Messages);
const enPaths = leafPaths(en as Messages);
it('契约 ErrorCodes 枚举全集在 errorCodeI18nKeys 无缺漏', () => {
const codes = Object.values(ErrorCodes);
expect(codes.length).toBeGreaterThanOrEqual(18); // 契约现存 18 码,只增不减
for (const code of codes) {
expect(errorCodeI18nKeys[code], `缺少错误码映射: ${code}`).toBeDefined();
}
});
it('映射表内每个文案键在 zh-CN 与 en 词条均存在且非空', () => {
for (const [code, key] of Object.entries(errorCodeI18nKeys)) {
for (const [locale, paths] of [
['zh-CN', zhPaths],
['en', enPaths],
] as const) {
expect(paths.has(key), `${code} 的文案键 ${key} 未登记于 ${locale} 词条`).toBe(true);
const value = resolvePath((locale === 'zh-CN' ? zhCN : en) as Messages, key);
expect(typeof value === 'string' && value.length > 0, `${key} 在 ${locale} 非非空字符串`).toBe(true);
}
}
});
it('zh-CN 与 en 词条键集合递归相等(无单侧漏译)', () => {
expect([...zhPaths].filter((path) => !enPaths.has(path))).toEqual([]);
expect([...enPaths].filter((path) => !zhPaths.has(path))).toEqual([]);
});
});