Phase 28 (c), the last of the phase. A word met inside a Portuguese document is a Portuguese card: migration 0018 mirrors documents.doc_lang onto vocab_words, set server-side from the ownership lookup capture was already making. Every card still reviews — filtering the queue to the half she is learning would drop the words she actually met. Read-aloud was the larger surprise. detectLang routed Han/kana to Chinese and everything else to en-US, so the zh pair was accidentally right and every Latin pair wrong. doc_lang now reaches the client read-only on the document JSON, and docLang(text, verdict) answers for a passage taken out of it — with the script test still winning, because quoted Chinese must never be spelled out one "Chinese letter" at a time. Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
118 lines
4.1 KiB
TypeScript
118 lines
4.1 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
import { docLang, nativeLang, speak, stopSpeech } from './speech'
|
|
import { resetPackForTests, setPackLang } from '../i18n'
|
|
|
|
// Read-aloud has two jobs beyond "make a sound": ask for the right pace, and ask
|
|
// in the right language. Both are decided at the call site and travel in the
|
|
// request body, so this checks the body — the part a component author can get
|
|
// wrong without anything failing loudly.
|
|
|
|
let bodies: Array<Record<string, unknown>>
|
|
|
|
beforeEach(() => {
|
|
bodies = []
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn((_url: string, init: RequestInit) => {
|
|
bodies.push(JSON.parse(String(init.body)))
|
|
// Never resolves to audio: the fallback path needs no window.Audio here,
|
|
// and rejecting would run the Web Speech branch instead of the server one.
|
|
return new Promise(() => {})
|
|
}),
|
|
)
|
|
})
|
|
|
|
afterEach(() => {
|
|
stopSpeech()
|
|
vi.unstubAllGlobals()
|
|
resetPackForTests()
|
|
})
|
|
|
|
describe('speak', () => {
|
|
it('asks for the normal pace by default', () => {
|
|
speak('reception')
|
|
expect(bodies).toHaveLength(1)
|
|
expect(bodies[0]).toMatchObject({ text: 'reception', lang: 'en-US', slow: false })
|
|
})
|
|
|
|
it('asks for the slow replay when the slow control is used', () => {
|
|
speak('reception', undefined, true)
|
|
expect(bodies[0]).toMatchObject({ text: 'reception', slow: true })
|
|
})
|
|
|
|
it('still detects Chinese by script, so a zh selection is never read in English', () => {
|
|
speak('你好世界')
|
|
expect(bodies[0]).toMatchObject({ lang: 'zh-CN' })
|
|
})
|
|
|
|
it('sends nothing for empty text', () => {
|
|
speak(' ')
|
|
expect(bodies).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('nativeLang', () => {
|
|
// The voice for her own language comes from the pack, not from the letters.
|
|
// "comum" is spelled the same in both halves of the pt pair, so a detector
|
|
// would have to guess; the component that knows it is rendering her language
|
|
// says so instead.
|
|
it('follows the pair language', () => {
|
|
setPackLang('zh')
|
|
expect(nativeLang()).toBe('zh-CN')
|
|
setPackLang('pt-PT')
|
|
expect(nativeLang()).toBe('pt-PT')
|
|
setPackLang('fr')
|
|
expect(nativeLang()).toBe('fr-FR')
|
|
})
|
|
|
|
it('names a European Portuguese voice, never a Brazilian one', () => {
|
|
setPackLang('pt-PT')
|
|
expect(nativeLang()).not.toBe('pt-BR')
|
|
})
|
|
|
|
it('is what a Latin-pair lookup speaks the other reading in', () => {
|
|
setPackLang('pt-PT')
|
|
speak('comum', nativeLang())
|
|
expect(bodies[0]).toMatchObject({ text: 'comum', lang: 'pt-PT' })
|
|
})
|
|
|
|
it('speaks the French reading of a collision in French', () => {
|
|
// "chat" is the sharpest case in the fr pair: an English word, a French
|
|
// word, and the companion's own animal. Nothing about the letters says
|
|
// which — only the pack does.
|
|
setPackLang('fr')
|
|
speak('chat', nativeLang())
|
|
expect(bodies.at(-1)).toMatchObject({ text: 'chat', lang: 'fr-FR' })
|
|
})
|
|
})
|
|
|
|
describe('docLang', () => {
|
|
// The document's verdict is the answer for a passage lifted out of it, because
|
|
// for a Latin pair there is no other answer available: an English sentence and
|
|
// a Portuguese one are the same letters.
|
|
it('reads a flipped document in her own language', () => {
|
|
setPackLang('pt-PT')
|
|
expect(docLang('Ontem foi difícil.', 'pair')).toBe('pt-PT')
|
|
})
|
|
|
|
it('reads an English document in English, and treats the backfill as English', () => {
|
|
setPackLang('pt-PT')
|
|
expect(docLang('Yesterday was hard.', 'en')).toBe('en-US')
|
|
expect(docLang('Yesterday was hard.', '')).toBe('en-US')
|
|
})
|
|
|
|
it('lets the script win over the verdict, so quoted Chinese is never spelled out', () => {
|
|
// An English document quoting Chinese is 'en' by verdict, and the English
|
|
// voice reads Han characters one "Chinese letter" at a time — the one
|
|
// failure worse than silence.
|
|
setPackLang('zh')
|
|
expect(docLang('你好世界', 'en')).toBe('zh-CN')
|
|
})
|
|
|
|
it('is what the editor selection and the garden card ask for', () => {
|
|
setPackLang('fr')
|
|
speak('Le chat dort.', docLang('Le chat dort.', 'pair'))
|
|
expect(bodies.at(-1)).toMatchObject({ text: 'Le chat dort.', lang: 'fr-FR' })
|
|
})
|
|
})
|