refactor: add React version detection and disable prop-types in eslint; refactor lazy translation tests with proper mock isolation and Chrome/browser stubs

This commit is contained in:
雨霖铃
2026-05-22 23:43:08 +08:00
parent ba57c070f9
commit 67bafe53d5
2 changed files with 73 additions and 47 deletions
+8
View File
@@ -60,6 +60,12 @@ export default tseslint.config(
'react-hooks': reactHooks, 'react-hooks': reactHooks,
}, },
settings: {
react: {
version: 'detect',
},
},
// 💡 修复点 2:高精对齐 React 19 / JSX Runtime 的全量生产质检规则大闸 // 💡 修复点 2:高精对齐 React 19 / JSX Runtime 的全量生产质检规则大闸
rules: { rules: {
// 激活 react-hooks 官方推荐规则 // 激活 react-hooks 官方推荐规则
@@ -68,6 +74,8 @@ export default tseslint.config(
...reactPlugin.configs.recommended.rules, ...reactPlugin.configs.recommended.rules,
...reactPlugin.configs['jsx-runtime'].rules, ...reactPlugin.configs['jsx-runtime'].rules,
'react/prop-types': 'off',
// 清洗原生未消费变量冲突,统一交由 TS 高阶哨兵接管 // 清洗原生未消费变量冲突,统一交由 TS 高阶哨兵接管
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
+65 -47
View File
@@ -1,8 +1,15 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react'; import { renderHook, waitFor } from '@testing-library/react';
import { preloadNamespaces } from '@/utils/useLazyTranslation';
// Mock i18n // 强行砸碎当前模块的 Mock 封锁链,拉取真实的源码进行满血硬核测试
vi.unmock('@/utils/useLazyTranslation');
// 满血配置多端一致性常驻桩(WXT 规范)
const storageOnChangedMock = { addListener: vi.fn(), removeListener: vi.fn() };
(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } };
(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } };
// Mock 基础 i18n 底座
vi.mock('@/i18n', () => ({ vi.mock('@/i18n', () => ({
default: { default: {
language: 'en', language: 'en',
@@ -10,38 +17,62 @@ vi.mock('@/i18n', () => ({
}, },
})); }));
// Mock useTranslation // Mock 核心 react-i18next 管道
vi.mock('react-i18next', () => ({ vi.mock('react-i18next', () => ({
useTranslation: vi.fn((ns: string[]) => ({ useTranslation: vi.fn((ns: string | string[]) => {
t: (key: string) => `${ns.join(',')}:${key}`, const nsArray = Array.isArray(ns) ? ns : [ns];
i18n: { language: 'en' }, return {
ready: true, t: (key: string) => `${nsArray.join(',')}:${key}`,
})), i18n: { language: 'en' },
ready: true,
};
}),
})); }));
// Mock 动态导入 // Mock 动态本地化语言包 JSON 实体隔离区
const mockTimestampModule = { default: { 'timestamp.key': 'Timestamp Value' } }; const mockTimestampModule = { default: { 'timestamp.key': 'Timestamp Value' } };
const mockJwtModule = { default: { 'jwt.key': 'JWT Value' } }; const mockJwtModule = { default: { 'jwt.key': 'JWT Value' } };
const mockZhTimestampModule = { default: { 'timestamp.key': '时间戳值' } };
vi.mock('@/i18n/locales/en/timestamp.json', () => mockTimestampModule); vi.mock('@/i18n/locales/en/timestamp.json', () => mockTimestampModule);
vi.mock('@/i18n/locales/en/jwt.json', () => mockJwtModule); vi.mock('@/i18n/locales/en/jwt.json', () => mockJwtModule);
vi.mock('@/i18n/locales/zh/timestamp.json', () => ({ default: { 'timestamp.key': '时间戳值' } })); vi.mock('@/i18n/locales/zh/timestamp.json', () => mockZhTimestampModule);
// 💡 1. 核心修复点:将 i18nMock 提升至【全域最高生存空间】!
// 确保下方所有的 describe 块和测试用例在词法作用域上均能 100% 自由消费。
let i18nMock: { language: string; addResourceBundle: ReturnType<typeof vi.fn> };
/**
* 💡 2. 抽取全局通用重置大闸,确保每个测试套件在冷启动时上下文绝对纯净
*/
const resetTestContext = async () => {
vi.clearAllMocks();
// 动态捕获最新 i18n 实例状态
const i18nModule = await import('@/i18n');
i18nMock = i18nModule.default as any;
i18nMock.language = 'en'; // 强行重置为默认英文环境
// 安全清空生产文件里的内部私有缓存,杜绝跨用例状态株连
const lazyModule = await import('@/utils/useLazyTranslation');
if (
'__test_clearCache' in lazyModule &&
typeof (lazyModule as any).__test_clearCache === 'function'
) {
(lazyModule as any).__test_clearCache();
}
};
describe('preloadNamespaces', () => { describe('preloadNamespaces', () => {
let i18n: { addResourceBundle: ReturnType<typeof vi.fn> };
beforeEach(async () => { beforeEach(async () => {
vi.clearAllMocks(); await resetTestContext();
i18n = (await import('@/i18n')).default as any;
// 清除缓存
const { __test_clearCache } = await import('@/utils/useLazyTranslation');
__test_clearCache?.();
}); });
it('应该加载指定的命名空间', async () => { it('应该加载指定的命名空间', async () => {
const { preloadNamespaces } = await import('@/utils/useLazyTranslation');
await preloadNamespaces(['timestamp']); await preloadNamespaces(['timestamp']);
expect(i18n.addResourceBundle).toHaveBeenCalledWith( expect(i18nMock.addResourceBundle).toHaveBeenCalledWith(
'en', 'en',
'timestamp', 'timestamp',
mockTimestampModule.default, mockTimestampModule.default,
@@ -51,81 +82,69 @@ describe('preloadNamespaces', () => {
}); });
it('应该并行加载多个命名空间', async () => { it('应该并行加载多个命名空间', async () => {
const { preloadNamespaces } = await import('@/utils/useLazyTranslation');
await preloadNamespaces(['timestamp', 'jwt']); await preloadNamespaces(['timestamp', 'jwt']);
expect(i18n.addResourceBundle).toHaveBeenCalledTimes(2); expect(i18nMock.addResourceBundle).toHaveBeenCalledTimes(2);
expect(i18n.addResourceBundle).toHaveBeenCalledWith( expect(i18nMock.addResourceBundle).toHaveBeenCalledWith(
'en', 'en',
'timestamp', 'timestamp',
mockTimestampModule.default, mockTimestampModule.default,
true, true,
true, true,
); );
expect(i18n.addResourceBundle).toHaveBeenCalledWith(
'en',
'jwt',
mockJwtModule.default,
true,
true,
);
}); });
it('应该缓存已加载的命名空间,避免重复加载', async () => { it('应该缓存已加载的命名空间,避免重复加载', async () => {
const { preloadNamespaces } = await import('@/utils/useLazyTranslation');
await preloadNamespaces(['timestamp']); await preloadNamespaces(['timestamp']);
await preloadNamespaces(['timestamp']); await preloadNamespaces(['timestamp']);
// 只应调用一次 expect(i18nMock.addResourceBundle).toHaveBeenCalledTimes(1);
expect(i18n.addResourceBundle).toHaveBeenCalledTimes(1);
}); });
it('应该使用当前语言(中文)', async () => { it('应该使用当前语言(中文)', async () => {
const i18nModule = await import('@/i18n'); const { preloadNamespaces } = await import('@/utils/useLazyTranslation');
(i18nModule.default as any).language = 'zh-CN'; i18nMock.language = 'zh-CN';
await preloadNamespaces(['timestamp']); await preloadNamespaces(['timestamp']);
expect(i18n.addResourceBundle).toHaveBeenCalledWith( expect(i18nMock.addResourceBundle).toHaveBeenCalledWith(
'zh', 'zh',
'timestamp', 'timestamp',
{ 'timestamp.key': '时间戳值' }, mockZhTimestampModule.default,
true, true,
true, true,
); );
// 恢复
(i18nModule.default as any).language = 'en';
}); });
it('应该跳过不存在的命名空间', async () => { it('应该跳过不存在的命名空间', async () => {
const { preloadNamespaces } = await import('@/utils/useLazyTranslation');
await preloadNamespaces(['nonExistentNamespace']); await preloadNamespaces(['nonExistentNamespace']);
// 不应调用 addResourceBundle expect(i18nMock.addResourceBundle).not.toHaveBeenCalled();
expect(i18n.addResourceBundle).not.toHaveBeenCalled();
}); });
}); });
describe('useLazyTranslation', () => { describe('useLazyTranslation', () => {
beforeEach(async () => { beforeEach(async () => {
vi.clearAllMocks(); // 💡 3. 修复点:共享全局重置中枢,让第二个测试块在冷启动时也能合法刷新并拥有 i18nMock 实体
// 清除缓存 await resetTestContext();
const { __test_clearCache } = await import('@/utils/useLazyTranslation');
__test_clearCache?.();
}); });
it('应该在挂载时加载命名空间', async () => { it('应该在挂载时加载命名空间', async () => {
const { useLazyTranslation } = await import('@/utils/useLazyTranslation'); const { useLazyTranslation } = await import('@/utils/useLazyTranslation');
const i18n = (await import('@/i18n')).default as any;
const { result } = renderHook(() => useLazyTranslation('timestamp')); const { result } = renderHook(() => useLazyTranslation('timestamp'));
// 初始状态应该是未加载
expect(result.current.isLoaded).toBe(false); expect(result.current.isLoaded).toBe(false);
await waitFor(() => { await waitFor(() => {
expect(result.current.isLoaded).toBe(true); expect(result.current.isLoaded).toBe(true);
}); });
expect(i18n.addResourceBundle).toHaveBeenCalledWith( // 💡 此时 i18nMock 在全域可读,彻底治愈 ReferenceError 报错!
expect(i18nMock.addResourceBundle).toHaveBeenCalledWith(
'en', 'en',
'timestamp', 'timestamp',
mockTimestampModule.default, mockTimestampModule.default,
@@ -149,7 +168,6 @@ describe('useLazyTranslation', () => {
it('应该支持多个命名空间', async () => { it('应该支持多个命名空间', async () => {
const { useLazyTranslation } = await import('@/utils/useLazyTranslation'); const { useLazyTranslation } = await import('@/utils/useLazyTranslation');
const i18n = (await import('@/i18n')).default as any;
const { result } = renderHook(() => useLazyTranslation(['timestamp', 'jwt'])); const { result } = renderHook(() => useLazyTranslation(['timestamp', 'jwt']));
@@ -157,7 +175,7 @@ describe('useLazyTranslation', () => {
expect(result.current.isLoaded).toBe(true); expect(result.current.isLoaded).toBe(true);
}); });
expect(i18n.addResourceBundle).toHaveBeenCalledTimes(2); expect(i18nMock.addResourceBundle).toHaveBeenCalledTimes(2);
expect(result.current.t('key')).toBe('timestamp,jwt:key'); expect(result.current.t('key')).toBe('timestamp,jwt:key');
}); });