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> 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' }) }) })