diff --git a/eslint.config.ts b/eslint.config.ts index fcae7f1..db7eb21 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -60,6 +60,12 @@ export default tseslint.config( 'react-hooks': reactHooks, }, + settings: { + react: { + version: 'detect', + }, + }, + // 💡 修复点 2:高精对齐 React 19 / JSX Runtime 的全量生产质检规则大闸 rules: { // 激活 react-hooks 官方推荐规则 @@ -68,6 +74,8 @@ export default tseslint.config( ...reactPlugin.configs.recommended.rules, ...reactPlugin.configs['jsx-runtime'].rules, + 'react/prop-types': 'off', + // 清洗原生未消费变量冲突,统一交由 TS 高阶哨兵接管 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': [ diff --git a/utils/__tests__/useLazyTranslation.test.ts b/utils/__tests__/useLazyTranslation.test.ts index 24f3c14..04b251d 100644 --- a/utils/__tests__/useLazyTranslation.test.ts +++ b/utils/__tests__/useLazyTranslation.test.ts @@ -1,8 +1,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; 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', () => ({ default: { language: 'en', @@ -10,38 +17,62 @@ vi.mock('@/i18n', () => ({ }, })); -// Mock useTranslation +// Mock 核心 react-i18next 管道 vi.mock('react-i18next', () => ({ - useTranslation: vi.fn((ns: string[]) => ({ - t: (key: string) => `${ns.join(',')}:${key}`, - i18n: { language: 'en' }, - ready: true, - })), + useTranslation: vi.fn((ns: string | string[]) => { + const nsArray = Array.isArray(ns) ? ns : [ns]; + return { + t: (key: string) => `${nsArray.join(',')}:${key}`, + i18n: { language: 'en' }, + ready: true, + }; + }), })); -// Mock 动态导入 +// Mock 动态本地化语言包 JSON 实体隔离区 const mockTimestampModule = { default: { 'timestamp.key': 'Timestamp 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/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 }; + +/** + * 💡 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', () => { - let i18n: { addResourceBundle: ReturnType }; - beforeEach(async () => { - vi.clearAllMocks(); - i18n = (await import('@/i18n')).default as any; - // 清除缓存 - const { __test_clearCache } = await import('@/utils/useLazyTranslation'); - __test_clearCache?.(); + await resetTestContext(); }); it('应该加载指定的命名空间', async () => { + const { preloadNamespaces } = await import('@/utils/useLazyTranslation'); await preloadNamespaces(['timestamp']); - expect(i18n.addResourceBundle).toHaveBeenCalledWith( + expect(i18nMock.addResourceBundle).toHaveBeenCalledWith( 'en', 'timestamp', mockTimestampModule.default, @@ -51,81 +82,69 @@ describe('preloadNamespaces', () => { }); it('应该并行加载多个命名空间', async () => { + const { preloadNamespaces } = await import('@/utils/useLazyTranslation'); await preloadNamespaces(['timestamp', 'jwt']); - expect(i18n.addResourceBundle).toHaveBeenCalledTimes(2); - expect(i18n.addResourceBundle).toHaveBeenCalledWith( + expect(i18nMock.addResourceBundle).toHaveBeenCalledTimes(2); + expect(i18nMock.addResourceBundle).toHaveBeenCalledWith( 'en', 'timestamp', mockTimestampModule.default, true, true, ); - expect(i18n.addResourceBundle).toHaveBeenCalledWith( - 'en', - 'jwt', - mockJwtModule.default, - true, - true, - ); }); it('应该缓存已加载的命名空间,避免重复加载', async () => { + const { preloadNamespaces } = await import('@/utils/useLazyTranslation'); await preloadNamespaces(['timestamp']); await preloadNamespaces(['timestamp']); - // 只应调用一次 - expect(i18n.addResourceBundle).toHaveBeenCalledTimes(1); + expect(i18nMock.addResourceBundle).toHaveBeenCalledTimes(1); }); it('应该使用当前语言(中文)', async () => { - const i18nModule = await import('@/i18n'); - (i18nModule.default as any).language = 'zh-CN'; + const { preloadNamespaces } = await import('@/utils/useLazyTranslation'); + i18nMock.language = 'zh-CN'; await preloadNamespaces(['timestamp']); - expect(i18n.addResourceBundle).toHaveBeenCalledWith( + expect(i18nMock.addResourceBundle).toHaveBeenCalledWith( 'zh', 'timestamp', - { 'timestamp.key': '时间戳值' }, + mockZhTimestampModule.default, true, true, ); - - // 恢复 - (i18nModule.default as any).language = 'en'; }); it('应该跳过不存在的命名空间', async () => { + const { preloadNamespaces } = await import('@/utils/useLazyTranslation'); await preloadNamespaces(['nonExistentNamespace']); - // 不应调用 addResourceBundle - expect(i18n.addResourceBundle).not.toHaveBeenCalled(); + expect(i18nMock.addResourceBundle).not.toHaveBeenCalled(); }); }); describe('useLazyTranslation', () => { beforeEach(async () => { - vi.clearAllMocks(); - // 清除缓存 - const { __test_clearCache } = await import('@/utils/useLazyTranslation'); - __test_clearCache?.(); + // 💡 3. 修复点:共享全局重置中枢,让第二个测试块在冷启动时也能合法刷新并拥有 i18nMock 实体 + await resetTestContext(); }); it('应该在挂载时加载命名空间', async () => { const { useLazyTranslation } = await import('@/utils/useLazyTranslation'); - const i18n = (await import('@/i18n')).default as any; const { result } = renderHook(() => useLazyTranslation('timestamp')); - // 初始状态应该是未加载 expect(result.current.isLoaded).toBe(false); await waitFor(() => { expect(result.current.isLoaded).toBe(true); }); - expect(i18n.addResourceBundle).toHaveBeenCalledWith( + // 💡 此时 i18nMock 在全域可读,彻底治愈 ReferenceError 报错! + expect(i18nMock.addResourceBundle).toHaveBeenCalledWith( 'en', 'timestamp', mockTimestampModule.default, @@ -149,7 +168,6 @@ describe('useLazyTranslation', () => { it('应该支持多个命名空间', async () => { const { useLazyTranslation } = await import('@/utils/useLazyTranslation'); - const i18n = (await import('@/i18n')).default as any; const { result } = renderHook(() => useLazyTranslation(['timestamp', 'jwt'])); @@ -157,7 +175,7 @@ describe('useLazyTranslation', () => { 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'); });