Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
169 lines
6.9 KiB
TypeScript
169 lines
6.9 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
|
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
|
import { getDictionaryService } from '../../src/main/services/DictionaryService'
|
|
import { registerDictionaryHandlers } from '../../src/main/ipc/dictionary-handlers'
|
|
import { invokeIpc, useRedHarness } from './harness'
|
|
import { USER_TEXT } from './fixtures'
|
|
|
|
useRedHarness()
|
|
|
|
describe('유스케이스: 사용자 사전 CRUD / 검색 / 힌트 / IPC', () => {
|
|
it('빈 사전 목록은 0건이다', () => {
|
|
const page = getDictionaryService().list({ page: 0, pageSize: 20 })
|
|
expect(page.total).toBe(0)
|
|
expect(page.entries).toEqual([])
|
|
})
|
|
|
|
it('단어를 추가하면 목록에 나타난다', () => {
|
|
const entry = getDictionaryService().add({ word: 'D3RO' })
|
|
expect(entry.word).toBe('D3RO')
|
|
expect(entry.category).toBe('user')
|
|
expect(entry.usageCount).toBe(0)
|
|
expect(getDictionaryService().list({ page: 0, pageSize: 10 }).total).toBe(1)
|
|
})
|
|
|
|
it('발음과 기술 카테고리를 지정해 추가한다', () => {
|
|
const entry = getDictionaryService().add({
|
|
word: 'Whisper',
|
|
pronunciation: '위스퍼',
|
|
category: 'technical',
|
|
})
|
|
expect(entry.pronunciation).toBe('위스퍼')
|
|
expect(entry.category).toBe('technical')
|
|
})
|
|
|
|
it('유니코드 단어도 추가된다', () => {
|
|
const entry = getDictionaryService().add({ word: USER_TEXT.UNICODE })
|
|
expect(entry.word).toBe(USER_TEXT.UNICODE)
|
|
})
|
|
|
|
it('빈 단어 추가는 거부된다', () => {
|
|
expect(() => getDictionaryService().add({ word: ' ' })).toThrow(D3ROError)
|
|
try {
|
|
getDictionaryService().add({ word: '' })
|
|
expect.unreachable()
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(D3ROError)
|
|
expect((err as D3ROError).code).toBe(ErrorCode.DictionaryImportInvalidFormat)
|
|
}
|
|
})
|
|
|
|
it('같은 단어+카테고리 중복 추가는 DictionaryDuplicate 이다', () => {
|
|
getDictionaryService().add({ word: USER_TEXT.DUP })
|
|
expect(() => getDictionaryService().add({ word: USER_TEXT.DUP })).toThrow(D3ROError)
|
|
try {
|
|
getDictionaryService().add({ word: USER_TEXT.DUP })
|
|
} catch (err) {
|
|
expect((err as D3ROError).code).toBe(ErrorCode.DictionaryDuplicate)
|
|
}
|
|
})
|
|
|
|
it('같은 단어를 다른 카테고리로 추가할 수 있다', () => {
|
|
getDictionaryService().add({ word: 'API', category: 'user' })
|
|
const tech = getDictionaryService().add({ word: 'API', category: 'technical' })
|
|
expect(tech.category).toBe('technical')
|
|
expect(getDictionaryService().list({ page: 0, pageSize: 10 }).total).toBe(2)
|
|
})
|
|
|
|
it('단어 수정이 반영된다', () => {
|
|
const created = getDictionaryService().add({ word: 'old' })
|
|
const updated = getDictionaryService().update({ id: created.id, word: 'new' })
|
|
expect(updated?.word).toBe('new')
|
|
expect(getDictionaryService().list({ page: 0, pageSize: 10 }).entries[0].word).toBe('new')
|
|
})
|
|
|
|
it('없는 id 수정은 null 이다', () => {
|
|
expect(getDictionaryService().update({ id: 'missing', word: 'x' })).toBeNull()
|
|
})
|
|
|
|
it('단어 삭제 후 목록에서 사라진다', () => {
|
|
const created = getDictionaryService().add({ word: 'gone' })
|
|
expect(getDictionaryService().delete(created.id)).toBe(true)
|
|
expect(getDictionaryService().list({ page: 0, pageSize: 10 }).total).toBe(0)
|
|
})
|
|
|
|
it('없는 id 삭제는 false 이다', () => {
|
|
expect(getDictionaryService().delete('missing')).toBe(false)
|
|
})
|
|
|
|
it('검색은 부분 일치한다', () => {
|
|
getDictionaryService().add({ word: '인공지능' })
|
|
getDictionaryService().add({ word: '자연어' })
|
|
const found = getDictionaryService().search({ query: '인공', page: 0, pageSize: 10 })
|
|
expect(found.total).toBe(1)
|
|
expect(found.entries[0].word).toBe('인공지능')
|
|
})
|
|
|
|
it('검색 무결과는 빈 페이지이다', () => {
|
|
getDictionaryService().add({ word: 'foo' })
|
|
expect(getDictionaryService().search({ query: 'zzz', page: 0, pageSize: 10 }).total).toBe(0)
|
|
})
|
|
|
|
it('사용 횟수 증가가 저장된다', () => {
|
|
const created = getDictionaryService().add({ word: 'hint' })
|
|
getDictionaryService().incrementUsage(created.id)
|
|
getDictionaryService().incrementUsage(created.id)
|
|
const listed = getDictionaryService().list({ page: 0, pageSize: 10 }).entries[0]
|
|
expect(listed.usageCount).toBe(2)
|
|
expect(listed.lastUsedAt).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('getPromptHints 는 사용 빈도 순 단어 목록이다', () => {
|
|
const a = getDictionaryService().add({ word: 'alpha' })
|
|
getDictionaryService().add({ word: 'beta' })
|
|
getDictionaryService().incrementUsage(a.id)
|
|
const hints = getDictionaryService().getPromptHints()
|
|
expect(hints.startsWith('alpha')).toBe(true)
|
|
expect(hints).toContain('beta')
|
|
})
|
|
|
|
it('빈 사전의 prompt hint 는 빈 문자열이다', () => {
|
|
expect(getDictionaryService().getPromptHints()).toBe('')
|
|
})
|
|
|
|
it('IPC dictionary:add / getAll 왕복', async () => {
|
|
registerDictionaryHandlers()
|
|
const add = await invokeIpc(IPC_CHANNELS.DICTIONARY.ADD, { word: 'IPCWORD' })
|
|
expect(add.success).toBe(true)
|
|
const all = await invokeIpc(IPC_CHANNELS.DICTIONARY.GET_ALL, { page: 0, pageSize: 10 })
|
|
expect(all.success).toBe(true)
|
|
if (all.success) expect(all.data.total).toBe(1)
|
|
})
|
|
|
|
it('IPC dictionary:add 빈 단어는 실패한다', async () => {
|
|
registerDictionaryHandlers()
|
|
const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.ADD, { word: ' ' })
|
|
expect(res.success).toBe(false)
|
|
})
|
|
|
|
it('IPC dictionary:add 중복은 DictionaryDuplicate 이다', async () => {
|
|
registerDictionaryHandlers()
|
|
await invokeIpc(IPC_CHANNELS.DICTIONARY.ADD, { word: 'dup' })
|
|
const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.ADD, { word: 'dup' })
|
|
expect(res.success).toBe(false)
|
|
if (!res.success) expect(res.error.code).toBe(ErrorCode.DictionaryDuplicate)
|
|
})
|
|
|
|
it('IPC dictionary:update 없는 id 는 DictionaryNotFound 이다', async () => {
|
|
registerDictionaryHandlers()
|
|
const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.UPDATE, { id: 'missing', word: 'x' })
|
|
expect(res.success).toBe(false)
|
|
if (!res.success) expect(res.error.code).toBe(ErrorCode.DictionaryNotFound)
|
|
})
|
|
|
|
it('IPC dictionary:delete 없는 id 는 DictionaryNotFound 이다', async () => {
|
|
registerDictionaryHandlers()
|
|
const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.DELETE, { id: 'missing' })
|
|
expect(res.success).toBe(false)
|
|
if (!res.success) expect(res.error.code).toBe(ErrorCode.DictionaryNotFound)
|
|
})
|
|
|
|
it('IPC dictionary:search 는 서비스 검색을 노출한다', async () => {
|
|
registerDictionaryHandlers()
|
|
getDictionaryService().add({ word: '검색대상' })
|
|
const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.SEARCH, { query: '검색', page: 0, pageSize: 10 })
|
|
expect(res.success).toBe(true)
|
|
if (res.success) expect(res.data.total).toBe(1)
|
|
})
|
|
})
|