## 核心架构重构 - 创建IImportExportable接口,定义统一的导入导出规范 - 新增ImportExportError错误类,提供专门的错误处理 - 将导入导出逻辑从集中式DataManager分散到各个服务 - DataManager职责精简:从375行减至67行(-308行),仅负责协调 ## 存储键架构优化 - 移动storage-keys.ts从ui包到core包,实现统一管理 - 修复存储键双重用途问题:物理存储键vs逻辑JSON导出键 - PreferenceService添加'pref:'前缀处理物理存储 - 解决数据导出不完整的关键架构缺陷 ## 服务层改造 ### ModelManager - 实现IImportExportable接口(+209行,-153行) - 添加exportData/importData/validateData方法 - 保持向后兼容的数据格式 ### TemplateManager - 实现分布式导入导出逻辑 - 移除过度设计的configurable storageKey - 统一使用PreferenceService管理用户偏好 ### HistoryManager - 添加完整的导入导出实现 - 支持数据验证和错误处理 ### PreferenceService - 实现统一的用户设置导入导出 - 处理存储键前缀转换逻辑 - 支持builtin-template-language等核心设置 ## Electron桌面端更新 - main.js: 新增148行IPC处理逻辑 - preload.js: 新增177行API暴露 - 更新所有service proxy类支持新接口 - 保持IPC通信的类型安全 ## 测试体系完善 - 新增各服务专门的import-export.test.ts文件 - 创建data/import-export-integration.test.ts集成测试 - 建立AI自动化测试框架验证存储键一致性 - 更新现有测试适配新架构 ## 文档与架构说明 - 创建import-export-interface-design.md设计文档 - 添加storage-key-architecture.md架构说明 - 建立AI自动化测试文档体系 - 更新workspace文档记录重构过程 BREAKING CHANGE: 导入导出接口从集中式DataManager重构为分布式服务实现, 各服务现在必须实现IImportExportable接口,存储键架构发生变化
175 lines
6.2 KiB
TypeScript
175 lines
6.2 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { TemplateLanguageService } from '../../../src/services/template/languageService';
|
|
import { createMockStorage } from '../../mocks/mockStorage';
|
|
import { PreferenceService } from '../../../src/services/preference/service';
|
|
import { MemoryStorageProvider } from '../../../src/services/storage/memoryStorageProvider';
|
|
|
|
describe('TemplateLanguageService', () => {
|
|
let service: TemplateLanguageService;
|
|
let mockStorage: ReturnType<typeof createMockStorage>;
|
|
let mockPreferenceService: PreferenceService;
|
|
|
|
beforeEach(() => {
|
|
// Mock navigator.language to English for consistent test environment
|
|
Object.defineProperty(navigator, 'language', {
|
|
value: 'en-US',
|
|
configurable: true
|
|
});
|
|
|
|
mockStorage = createMockStorage();
|
|
const memoryStorage = new MemoryStorageProvider();
|
|
mockPreferenceService = new PreferenceService(memoryStorage);
|
|
service = new TemplateLanguageService(mockPreferenceService);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('initialization', () => {
|
|
it('should initialize with default language when no saved preference', async () => {
|
|
mockStorage.getItem.mockResolvedValue(null);
|
|
|
|
await service.initialize();
|
|
|
|
expect(await service.getCurrentLanguage()).toBe('en-US');
|
|
expect(service.isInitialized()).toBe(true);
|
|
});
|
|
|
|
it('should load saved language preference', async () => {
|
|
// Mock PreferenceService to return saved language
|
|
vi.spyOn(mockPreferenceService, 'get').mockResolvedValue('en-US');
|
|
|
|
await service.initialize();
|
|
|
|
expect(await service.getCurrentLanguage()).toBe('en-US');
|
|
expect(mockPreferenceService.get).toHaveBeenCalledWith('app:settings:ui:builtin-template-language', null);
|
|
});
|
|
|
|
it('should fallback to default language on invalid saved preference', async () => {
|
|
vi.spyOn(mockPreferenceService, 'get').mockResolvedValue('invalid-lang');
|
|
|
|
await service.initialize();
|
|
|
|
expect(await service.getCurrentLanguage()).toBe('en-US');
|
|
});
|
|
|
|
it('should handle storage errors gracefully', async () => {
|
|
vi.spyOn(mockPreferenceService, 'get').mockRejectedValue(new Error('Storage error'));
|
|
|
|
await service.initialize();
|
|
|
|
expect(await service.getCurrentLanguage()).toBe('en-US');
|
|
expect(service.isInitialized()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('language management', () => {
|
|
beforeEach(async () => {
|
|
await service.initialize();
|
|
});
|
|
|
|
it('should set language successfully', async () => {
|
|
vi.spyOn(mockPreferenceService, 'set').mockResolvedValue();
|
|
|
|
await service.setLanguage('en-US');
|
|
|
|
expect(await service.getCurrentLanguage()).toBe('en-US');
|
|
expect(mockPreferenceService.set).toHaveBeenCalledWith('app:settings:ui:builtin-template-language', 'en-US');
|
|
});
|
|
|
|
it('should reject invalid language', async () => {
|
|
await expect(service.setLanguage('invalid-lang' as any)).rejects.toThrow(
|
|
'Unsupported language: invalid-lang'
|
|
);
|
|
});
|
|
|
|
it('should toggle between languages', async () => {
|
|
// Start with detected language (en-US in test environment)
|
|
expect(await service.getCurrentLanguage()).toBe('en-US');
|
|
|
|
// Toggle to Chinese
|
|
const newLang1 = await service.toggleLanguage();
|
|
expect(newLang1).toBe('zh-CN');
|
|
expect(await service.getCurrentLanguage()).toBe('zh-CN');
|
|
|
|
// Toggle back to English
|
|
const newLang2 = await service.toggleLanguage();
|
|
expect(newLang2).toBe('en-US');
|
|
expect(await service.getCurrentLanguage()).toBe('en-US');
|
|
});
|
|
});
|
|
|
|
describe('utility methods', () => {
|
|
it('should validate languages correctly', async () => {
|
|
expect(await service.isValidLanguage('zh-CN')).toBe(true);
|
|
expect(await service.isValidLanguage('en-US')).toBe(true);
|
|
expect(await service.isValidLanguage('fr-FR')).toBe(false);
|
|
expect(await service.isValidLanguage('')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('browser language detection', () => {
|
|
it('should detect Chinese browser language', async () => {
|
|
// Mock navigator.language
|
|
Object.defineProperty(navigator, 'language', {
|
|
value: 'zh-CN',
|
|
configurable: true
|
|
});
|
|
|
|
const memoryStorage = new MemoryStorageProvider();
|
|
const newPreferenceService = new PreferenceService(memoryStorage);
|
|
const newService = new TemplateLanguageService(newPreferenceService);
|
|
await newService.initialize();
|
|
|
|
expect(await newService.getCurrentLanguage()).toBe('zh-CN');
|
|
});
|
|
|
|
it('should detect English browser language', async () => {
|
|
// Mock navigator.language
|
|
Object.defineProperty(navigator, 'language', {
|
|
value: 'en-US',
|
|
configurable: true
|
|
});
|
|
|
|
const memoryStorage = new MemoryStorageProvider();
|
|
const newPreferenceService = new PreferenceService(memoryStorage);
|
|
const newService = new TemplateLanguageService(newPreferenceService);
|
|
await newService.initialize();
|
|
|
|
expect(await newService.getCurrentLanguage()).toBe('en-US');
|
|
});
|
|
|
|
it('should default to English for unsupported browser languages', async () => {
|
|
// Mock navigator.language
|
|
Object.defineProperty(navigator, 'language', {
|
|
value: 'fr-FR',
|
|
configurable: true
|
|
});
|
|
|
|
const memoryStorage = new MemoryStorageProvider();
|
|
const newPreferenceService = new PreferenceService(memoryStorage);
|
|
const newService = new TemplateLanguageService(newPreferenceService);
|
|
await newService.initialize();
|
|
|
|
expect(await newService.getCurrentLanguage()).toBe('en-US');
|
|
});
|
|
});
|
|
|
|
describe('instance behavior', () => {
|
|
it('should create independent instances', () => {
|
|
const memoryStorage1 = new MemoryStorageProvider();
|
|
const preferenceService1 = new PreferenceService(memoryStorage1);
|
|
const memoryStorage2 = new MemoryStorageProvider();
|
|
const preferenceService2 = new PreferenceService(memoryStorage2);
|
|
|
|
const instance1 = new TemplateLanguageService(preferenceService1);
|
|
const instance2 = new TemplateLanguageService(preferenceService2);
|
|
|
|
expect(instance1).not.toBe(instance2);
|
|
expect(instance1).toBeInstanceOf(TemplateLanguageService);
|
|
expect(instance2).toBeInstanceOf(TemplateLanguageService);
|
|
});
|
|
});
|
|
});
|