initialize Electron desktop app and remove unused files
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useBrowserSpeechRecognition } from '../../packages/client/src/composables/useBrowserSpeechRecognition'
|
||||
|
||||
class FakeSpeechRecognition {
|
||||
static instances: FakeSpeechRecognition[] = []
|
||||
|
||||
lang = ''
|
||||
interimResults = false
|
||||
continuous = false
|
||||
onresult: ((event: any) => void) | null = null
|
||||
onerror: ((event: any) => void) | null = null
|
||||
onend: (() => void) | null = null
|
||||
started = false
|
||||
stopCalls = 0
|
||||
abortCalls = 0
|
||||
|
||||
constructor() {
|
||||
FakeSpeechRecognition.instances.push(this)
|
||||
}
|
||||
|
||||
start() {
|
||||
this.started = true
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.stopCalls += 1
|
||||
}
|
||||
|
||||
abort() {
|
||||
this.abortCalls += 1
|
||||
}
|
||||
|
||||
emitResult(results: Array<{ transcript: string; isFinal: boolean }>, resultIndex = 0) {
|
||||
this.onresult?.({
|
||||
resultIndex,
|
||||
results: results.map(result => ({
|
||||
isFinal: result.isFinal,
|
||||
0: { transcript: result.transcript },
|
||||
length: 1,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
emitError(error: string) {
|
||||
this.onerror?.({ error })
|
||||
}
|
||||
|
||||
emitEnd() {
|
||||
this.onend?.()
|
||||
}
|
||||
}
|
||||
|
||||
function installSpeechRecognition(globalName: 'SpeechRecognition' | 'webkitSpeechRecognition' = 'SpeechRecognition') {
|
||||
const browserWindow = window as Window & {
|
||||
SpeechRecognition?: typeof FakeSpeechRecognition
|
||||
webkitSpeechRecognition?: typeof FakeSpeechRecognition
|
||||
}
|
||||
|
||||
delete browserWindow.SpeechRecognition
|
||||
delete browserWindow.webkitSpeechRecognition
|
||||
browserWindow[globalName] = FakeSpeechRecognition
|
||||
}
|
||||
|
||||
function removeSpeechRecognition() {
|
||||
const browserWindow = window as Window & {
|
||||
SpeechRecognition?: typeof FakeSpeechRecognition
|
||||
webkitSpeechRecognition?: typeof FakeSpeechRecognition
|
||||
}
|
||||
|
||||
delete browserWindow.SpeechRecognition
|
||||
delete browserWindow.webkitSpeechRecognition
|
||||
FakeSpeechRecognition.instances = []
|
||||
}
|
||||
|
||||
describe('useBrowserSpeechRecognition', () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers()
|
||||
removeSpeechRecognition()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
removeSpeechRecognition()
|
||||
})
|
||||
|
||||
it('detects SpeechRecognition and webkitSpeechRecognition availability', () => {
|
||||
installSpeechRecognition('SpeechRecognition')
|
||||
expect(useBrowserSpeechRecognition().isSupported.value).toBe(true)
|
||||
|
||||
removeSpeechRecognition()
|
||||
installSpeechRecognition('webkitSpeechRecognition')
|
||||
expect(useBrowserSpeechRecognition().isSupported.value).toBe(true)
|
||||
|
||||
removeSpeechRecognition()
|
||||
expect(useBrowserSpeechRecognition().isSupported.value).toBe(false)
|
||||
})
|
||||
|
||||
it('start creates a recognizer, applies language settings, and enters listening state', async () => {
|
||||
installSpeechRecognition()
|
||||
const recognition = useBrowserSpeechRecognition()
|
||||
|
||||
await recognition.start({ language: 'en-US' })
|
||||
|
||||
const instance = FakeSpeechRecognition.instances[0]
|
||||
expect(instance).toBeDefined()
|
||||
expect(instance.lang).toBe('en-US')
|
||||
expect(instance.interimResults).toBe(true)
|
||||
expect(instance.continuous).toBe(true)
|
||||
expect(instance.started).toBe(true)
|
||||
expect(recognition.status.value).toBe('listening')
|
||||
expect(recognition.error.value).toBeNull()
|
||||
})
|
||||
|
||||
it('records interim and final transcripts from onresult callbacks', async () => {
|
||||
installSpeechRecognition()
|
||||
const recognition = useBrowserSpeechRecognition()
|
||||
|
||||
await recognition.start({ language: 'en' })
|
||||
const instance = FakeSpeechRecognition.instances[0]
|
||||
|
||||
instance.emitResult([{ transcript: ' hello world ', isFinal: false }])
|
||||
expect(recognition.partialTranscript.value).toBe('hello world')
|
||||
expect(recognition.transcript.value).toBe('')
|
||||
|
||||
instance.emitResult([{ transcript: ' hello world ', isFinal: true }])
|
||||
expect(recognition.transcript.value).toBe('hello world')
|
||||
expect(recognition.partialTranscript.value).toBe('')
|
||||
})
|
||||
|
||||
it('stop waits for final result and onend, then returns normalized transcript', async () => {
|
||||
installSpeechRecognition()
|
||||
const recognition = useBrowserSpeechRecognition()
|
||||
|
||||
await recognition.start({ language: 'en' })
|
||||
const instance = FakeSpeechRecognition.instances[0]
|
||||
const stopPromise = recognition.stop()
|
||||
|
||||
expect(instance.stopCalls).toBe(1)
|
||||
expect(recognition.status.value).toBe('stopping')
|
||||
|
||||
instance.emitResult([{ transcript: ' hello hermes ', isFinal: true }])
|
||||
instance.emitEnd()
|
||||
|
||||
await expect(stopPromise).resolves.toBe('hello hermes')
|
||||
expect(recognition.status.value).toBe('idle')
|
||||
})
|
||||
|
||||
it('stop promotes the latest interim transcript when recognition ends without a final result', async () => {
|
||||
installSpeechRecognition()
|
||||
const recognition = useBrowserSpeechRecognition()
|
||||
|
||||
await recognition.start({ language: 'en' })
|
||||
const instance = FakeSpeechRecognition.instances[0]
|
||||
|
||||
instance.emitResult([{ transcript: ' hello from interim ', isFinal: false }])
|
||||
|
||||
const stopPromise = recognition.stop()
|
||||
|
||||
expect(instance.stopCalls).toBe(1)
|
||||
instance.emitEnd()
|
||||
|
||||
await expect(stopPromise).resolves.toBe('hello from interim')
|
||||
expect(recognition.status.value).toBe('idle')
|
||||
})
|
||||
|
||||
it('stop timeout falls back to the latest interim transcript when no final result arrives', async () => {
|
||||
vi.useFakeTimers()
|
||||
installSpeechRecognition()
|
||||
const recognition = useBrowserSpeechRecognition()
|
||||
|
||||
await recognition.start({ language: 'en' })
|
||||
const instance = FakeSpeechRecognition.instances[0]
|
||||
instance.emitResult([{ transcript: ' hello timeout fallback ', isFinal: false }])
|
||||
|
||||
const stopPromise = recognition.stop()
|
||||
|
||||
expect(instance.stopCalls).toBe(1)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
|
||||
await expect(stopPromise).resolves.toBe('hello timeout fallback')
|
||||
expect(recognition.status.value).toBe('idle')
|
||||
})
|
||||
|
||||
it('restarts recognition after a natural browser end while still listening', async () => {
|
||||
installSpeechRecognition()
|
||||
const recognition = useBrowserSpeechRecognition()
|
||||
|
||||
await recognition.start({ language: 'en-US' })
|
||||
const first = FakeSpeechRecognition.instances[0]
|
||||
first.emitResult([{ transcript: 'first sentence', isFinal: true }])
|
||||
first.emitEnd()
|
||||
|
||||
expect(recognition.status.value).toBe('listening')
|
||||
expect(recognition.transcript.value).toBe('first sentence')
|
||||
expect(FakeSpeechRecognition.instances).toHaveLength(2)
|
||||
const second = FakeSpeechRecognition.instances[1]
|
||||
expect(second.lang).toBe('en-US')
|
||||
expect(second.continuous).toBe(true)
|
||||
expect(second.started).toBe(true)
|
||||
|
||||
second.emitResult([{ transcript: 'second sentence', isFinal: true }])
|
||||
const stopPromise = recognition.stop()
|
||||
second.emitEnd()
|
||||
|
||||
await expect(stopPromise).resolves.toBe('first sentence second sentence')
|
||||
expect(recognition.status.value).toBe('idle')
|
||||
})
|
||||
|
||||
it('cancel aborts the active recognizer, clears state, and ignores stale final callbacks', async () => {
|
||||
installSpeechRecognition()
|
||||
const recognition = useBrowserSpeechRecognition()
|
||||
|
||||
await recognition.start({ language: 'en' })
|
||||
const instance = FakeSpeechRecognition.instances[0]
|
||||
|
||||
recognition.cancel()
|
||||
|
||||
expect(instance.abortCalls).toBe(1)
|
||||
expect(recognition.status.value).toBe('idle')
|
||||
expect(recognition.transcript.value).toBe('')
|
||||
expect(recognition.partialTranscript.value).toBe('')
|
||||
|
||||
instance.emitResult([{ transcript: 'stale result', isFinal: true }])
|
||||
instance.emitEnd()
|
||||
|
||||
expect(recognition.transcript.value).toBe('')
|
||||
expect(recognition.partialTranscript.value).toBe('')
|
||||
})
|
||||
|
||||
it('ignores stale callbacks from older recognition sessions', async () => {
|
||||
installSpeechRecognition()
|
||||
const recognition = useBrowserSpeechRecognition()
|
||||
|
||||
await recognition.start({ language: 'en' })
|
||||
const first = FakeSpeechRecognition.instances[0]
|
||||
|
||||
await recognition.start({ language: 'fr' })
|
||||
const second = FakeSpeechRecognition.instances[1]
|
||||
|
||||
first.emitResult([{ transcript: 'old transcript', isFinal: true }])
|
||||
first.emitEnd()
|
||||
|
||||
expect(recognition.transcript.value).toBe('')
|
||||
expect(recognition.status.value).toBe('listening')
|
||||
|
||||
second.emitResult([{ transcript: 'bonjour', isFinal: false }])
|
||||
expect(recognition.partialTranscript.value).toBe('bonjour')
|
||||
})
|
||||
|
||||
it('reports a clear unsupported error when the browser API is unavailable', async () => {
|
||||
const recognition = useBrowserSpeechRecognition()
|
||||
|
||||
await expect(recognition.start({ language: 'en' })).rejects.toThrow(
|
||||
'Browser speech recognition is not supported in this browser.',
|
||||
)
|
||||
expect(recognition.status.value).toBe('error')
|
||||
expect(recognition.error.value?.message).toBe('Browser speech recognition is not supported in this browser.')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user