// tests/main/services/DictionaryIo.test.ts // DictionaryService import/export — file + dialog mocking import { describe, it, expect, beforeEach, vi } from 'vitest' const writeFileSync = vi.fn() const readFileSync = vi.fn() const showSaveDialog = vi.fn() vi.mock('fs', () => ({ default: { writeFileSync, readFileSync }, writeFileSync, readFileSync })) vi.mock('electron', () => ({ app: { getPath: () => '/tmp/d3ro' }, dialog: { showSaveDialog } })) vi.mock('../../../src/main/windows/WindowManager', () => ({ getMainWindow: () => null })) vi.mock('../../../src/main/services/LoggerService', () => ({ getLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }) })) vi.mock('../../../src/main/services/CloudSyncService', () => ({ getCloudSyncService: () => ({ pushOne: vi.fn(async () => undefined) }) })) interface TxStub { select: ReturnType insert: ReturnType } let existingRow: unknown = null let txStub: TxStub const mockDb = { select: vi.fn(() => ({ from: vi.fn(() => ({ orderBy: vi.fn(() => ({ all: vi.fn(() => [ { id: 'row-1', word: 'AI', pronunciation: '에이아이', category: 'technical', usageCount: 3, lastUsedAt: 111, createdAt: 100, updatedAt: 200 } ]) })) })) })), transaction: vi.fn((fn: (tx: TxStub) => void) => fn(txStub)) } vi.mock('../../../src/main/db', () => ({ getDatabase: () => mockDb })) type ServiceModule = typeof import('../../../src/main/services/DictionaryService') let mod: ServiceModule beforeEach(async () => { vi.clearAllMocks() existingRow = null txStub = { select: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ get: vi.fn(() => existingRow) })) })) })), insert: vi.fn(() => ({ values: vi.fn(() => ({ run: vi.fn(() => ({ changes: 1 })) })) })) } vi.resetModules() mod = await import('../../../src/main/services/DictionaryService') }) describe('DictionaryService.importDictionary', () => { it('imports JSON entries with camelCase fields', () => { readFileSync.mockReturnValue( JSON.stringify({ entries: [ { word: '쿠버네티스', pronunciation: '쿠버네티스', category: 'technical', usageCount: 2 } ] }) ) const result = mod.getDictionaryService().importDictionary({ filePath: '/in/dict.json', format: 'json' }) expect(result).toEqual({ imported: 1, skipped: 0, errors: 0 }) expect(txStub.insert).toHaveBeenCalled() }) it('skips entries that already exist', () => { existingRow = { id: 'existing' } readFileSync.mockReturnValue(JSON.stringify({ entries: [{ word: 'AI' }] })) const result = mod.getDictionaryService().importDictionary({ filePath: '/in/dict.json', format: 'json' }) expect(result).toEqual({ imported: 0, skipped: 1, errors: 0 }) }) it('parses CSV exports', () => { readFileSync.mockReturnValue( 'word,pronunciation,category,usageCount,createdAt,updatedAt\r\n"테스트",,"user",0,100,200\r\n' ) const result = mod.getDictionaryService().importDictionary({ filePath: '/in/dict.csv', format: 'csv' }) expect(result).toEqual({ imported: 1, skipped: 0, errors: 0 }) }) it('rejects a file without a word column', () => { readFileSync.mockReturnValue('term,category\r\nfoo,user\r\n') expect(() => mod.getDictionaryService().importDictionary({ filePath: '/in/dict.csv', format: 'csv' }) ).toThrowError(/Invalid dictionary file/) }) it('counts invalid JSON entries as errors', () => { readFileSync.mockReturnValue(JSON.stringify({ entries: [{ pronunciation: 'x' }, { word: 'ok' }] })) const result = mod.getDictionaryService().importDictionary({ filePath: '/in/dict.json', format: 'json' }) expect(result).toEqual({ imported: 1, skipped: 0, errors: 1 }) }) }) describe('DictionaryService.exportDictionary', () => { it('writes a JSON export and returns the chosen path', async () => { showSaveDialog.mockResolvedValue({ canceled: false, filePath: '/out/d3ro.json' }) const path = await mod.getDictionaryService().exportDictionary({ format: 'json' }) expect(path).toBe('/out/d3ro.json') expect(writeFileSync).toHaveBeenCalledTimes(1) const [writtenPath, content] = writeFileSync.mock.calls[0] as [string, string] expect(writtenPath).toBe('/out/d3ro.json') const parsed = JSON.parse(content) as { entries: Array<{ word: string }> } expect(parsed.entries[0].word).toBe('AI') }) it('writes CSV with a header row', async () => { showSaveDialog.mockResolvedValue({ canceled: false, filePath: '/out/d3ro.csv' }) await mod.getDictionaryService().exportDictionary({ format: 'csv' }) const [, content] = writeFileSync.mock.calls[0] as [string, string] expect(content).toContain('word,pronunciation,category,usageCount,createdAt,updatedAt') }) it('rejects a cancelled save dialog', async () => { showSaveDialog.mockResolvedValue({ canceled: true, filePath: undefined }) await expect( mod.getDictionaryService().exportDictionary({ format: 'json' }) ).rejects.toThrowError(/Export cancelled/) expect(writeFileSync).not.toHaveBeenCalled() }) })