Files
lexgo/admin/tests/dictionary-loader.test.mjs
T
ila b1b2e6c6fe feat: 英汉词典与规范音标、多词典并存 (#40)
- ECDICT 常用子集(82,721 条,含中文释义)与 CMUdict ARPAbet→IPA 转写音标,
  由 scripts/dict_prepare.py 从 sha256 pin 的源显式准备,运行时不联网。
- schema v12:放开 lexgo_dictionaries 单行约束并增加 provider 列,英英与英汉词典
  可分别启用;查词合并时中文释义在前、英英释义随后。
- 音标只取可验证来源:优先 CMUdict 转写,否则保留 ECDICT 记法并标注来源,
  两者都不可靠时不显示;含 ^ 等丢失首音的记法整条丢弃。
- 屈折形经 WordNet lemma 解析后回查中文词典,但不把词目音标复制到屈折形上。
- 管理端可区分两种词典并分别启停;学习端查词面板显示音标与中文释义。
2026-09-16 13:17:26 +08:00

85 lines
4.0 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
async function fixture() {
const { createDictionaryLoader } = await import('../src/dictionaries.mjs')
const pending = []
const later = () => new Promise((resolve, reject) => pending.push({ resolve, reject }))
const session = { state: { generation: 1 }, listDictionaries: later, importDictionary: later, setDictionaryEnabled: later }
const state = { items: [{ id: 1, name: 'Existing', enabled: true }], supported: null, loading: false, saving: false, error: '', notice: '' }
return { loader: createDictionaryLoader(session, state), state, session, pending }
}
test('failed upload preserves the existing resource and exposes the error', async () => {
const { loader, state, pending } = await fixture()
const action = loader.import(new FormData())
assert.equal(state.saving, true)
pending[0].reject(new Error('文件摘要不匹配'))
assert.equal(await action, false)
assert.equal(state.items[0].name, 'Existing')
assert.equal(state.error, '文件摘要不匹配')
assert.equal(state.saving, false)
})
test('accepted mutation replaces old state directly, without a second refresh dependency', async () => {
const { loader, state, pending } = await fixture()
const action = loader.toggle(1, false)
pending[0].resolve({ resource: { id: 1, name: 'Existing', enabled: false, status: 'disabled' } })
assert.equal(await action, true)
assert.equal(state.items[0].enabled, false)
assert.equal(pending.length, 1)
})
test('an old list cannot overwrite a newer import and duplicate import is explicit', async () => {
const { loader, state, pending } = await fixture()
const read = loader.load()
const action = loader.import(new FormData())
pending[1].resolve({ resource: { id: 1, name: 'Imported', enabled: true }, duplicate: true })
await action
pending[0].resolve({ items: [{ id: 1, name: 'Old' }] })
await read
assert.equal(state.items[0].name, 'Imported')
assert.match(state.notice, /已存在/)
})
test('page exit or session change discards late responses', async () => {
const { loader, state, pending, session } = await fixture()
const action = loader.import(new FormData())
loader.invalidate()
pending[0].resolve({ resource: { id: 1, name: 'Late' } })
assert.equal(await action, false)
assert.deepEqual(state.items, [])
const read = loader.load()
session.state.generation++
pending[1].resolve({ items: [{ id: 1, name: 'Other session' }] })
await read
assert.deepEqual(state.items, [])
})
test('both dictionaries stay independently switchable and keep their provider', async () => {
const { createDictionaryLoader } = await import('../src/dictionaries.mjs')
const state = { items: [], supported: null, loading: false, saving: false, error: '', notice: '' }
const supported = [
{ provider: 'wordnet', name: 'Princeton WordNet', version: '3.0', format: 'wordnet-3.0-zip', source: 'https://example.invalid/wordnet.zip', downloadable: true },
{ provider: 'ecdict', name: '简明英汉字典增强版', version: 'ecdict-1.0.28+cmudict-0.7b', format: 'lexgo-zh-dict-v1', source: 'https://example.invalid/ecdict-stardict-28.zip', downloadable: false }
]
const rows = [
{ id: 1, provider: 'wordnet', name: 'Princeton WordNet', enabled: true, status: 'ready' },
{ id: 2, provider: 'ecdict', name: '简明英汉字典增强版', enabled: true, status: 'ready' }
]
const session = {
state: { generation: 1 },
listDictionaries: async () => ({ items: rows, supported }),
importDictionary: async () => ({ resource: rows[1], duplicate: false }),
setDictionaryEnabled: async (id, enabled) => ({ resource: { ...rows.find(row => row.id === id), enabled } })
}
const loader = createDictionaryLoader(session, state)
assert.equal(await loader.load(), true)
assert.deepEqual(state.supported, supported)
assert.equal(state.items.length, 2)
assert.equal(await loader.toggle(2, false), true)
assert.equal(state.items[1].enabled, false)
assert.equal(state.items[0].enabled, true)
assert.match(state.notice, /停用/)
})