@untemps/react-vocal 2.0.0-beta.5 → 2.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +4 -1
  3. package/dist/index.es.js +99 -78
  4. package/dist/index.js +2 -2
  5. package/dist/index.umd.js +2 -2
  6. package/package.json +7 -1
  7. package/.github/workflows/publish.yml +0 -32
  8. package/.husky/commit-msg +0 -1
  9. package/.husky/pre-commit +0 -1
  10. package/.prettierignore +0 -3
  11. package/.prettierrc +0 -29
  12. package/CLAUDE.md +0 -59
  13. package/assets/icon-idle.png +0 -0
  14. package/assets/icon-listening.png +0 -0
  15. package/assets/microphone.png +0 -0
  16. package/assets/react-vocal.png +0 -0
  17. package/commitlint.config.js +0 -7
  18. package/dev/index.html +0 -24
  19. package/dev/package.json +0 -18
  20. package/dev/public/index.html +0 -24
  21. package/dev/src/index.jsx +0 -58
  22. package/dev/vite.config.js +0 -10
  23. package/dev/yarn.lock +0 -325
  24. package/dist/index.es.js.map +0 -1
  25. package/dist/index.js.map +0 -1
  26. package/dist/index.umd.js.map +0 -1
  27. package/src/components/Icon.jsx +0 -24
  28. package/src/components/Vocal.jsx +0 -233
  29. package/src/components/__tests__/Icon.test.jsx +0 -38
  30. package/src/components/__tests__/Vocal.test.jsx +0 -611
  31. package/src/components/__tests__/VocalWithMockedUseVocal.test.jsx +0 -38
  32. package/src/components/__tests__/__snapshots__/Icon.test.jsx.snap +0 -21
  33. package/src/components/__tests__/__snapshots__/Vocal.test.jsx.snap +0 -28
  34. package/src/hooks/__tests__/useCommands.test.js +0 -115
  35. package/src/hooks/__tests__/useTimeout.test.js +0 -69
  36. package/src/hooks/__tests__/useVocal.test.js +0 -202
  37. package/src/hooks/useCommands.js +0 -75
  38. package/src/hooks/useTimeout.js +0 -21
  39. package/src/hooks/useVocal.js +0 -56
  40. package/src/index.js +0 -7
  41. package/vite.config.js +0 -36
  42. package/vitest.setup.js +0 -77
@@ -1,38 +0,0 @@
1
- import React from 'react'
2
- import { render } from '@testing-library/react'
3
-
4
- import Icon from '../Icon'
5
-
6
- const defaultProps = {}
7
- const getInstance = (props = {}) => <Icon {...defaultProps} {...props} />
8
-
9
- describe('Icon', () => {
10
- it('matches snapshot', () => {
11
- const { asFragment } = render(getInstance())
12
- expect(asFragment()).toMatchSnapshot()
13
- })
14
-
15
- it('renders component', () => {
16
- const { queryByTestId } = render(getInstance())
17
- expect(queryByTestId('__icon-root__')).toBeInTheDocument()
18
- })
19
-
20
- it('renders component color', () => {
21
- const color = 'green'
22
- const { queryByTestId } = render(getInstance({ color }))
23
- expect(queryByTestId('__icon-path__')).toHaveAttribute('fill', color)
24
- })
25
-
26
- it('renders active component', () => {
27
- const isActive = true
28
- const { queryByTestId } = render(getInstance({ isActive }))
29
- expect(queryByTestId('__icon-active__')).toBeInTheDocument()
30
- })
31
-
32
- it('renders active component color', () => {
33
- const isActive = true
34
- const activeColor = 'blue'
35
- const { queryByTestId } = render(getInstance({ isActive, activeColor }))
36
- expect(queryByTestId('__icon-active__')).toHaveAttribute('fill', activeColor)
37
- })
38
- })
@@ -1,611 +0,0 @@
1
- import React from 'react'
2
- import { waitFor } from '@testing-library/dom'
3
- import { act, fireEvent, render } from '@testing-library/react'
4
- import { Vocal as SpeechRecognitionWrapper } from '@untemps/vocal'
5
-
6
- import Vocal from '../Vocal'
7
-
8
- const defaultProps = {}
9
- const getInstance = (props = {}, children = null) => (
10
- <Vocal {...defaultProps} {...props}>
11
- {children}
12
- </Vocal>
13
- )
14
-
15
- describe('Vocal', () => {
16
- it('matches snapshot', () => {
17
- const { asFragment } = render(getInstance())
18
- expect(asFragment()).toMatchSnapshot()
19
- })
20
-
21
- it('renders default children', () => {
22
- const { queryByTestId } = render(getInstance())
23
- expect(queryByTestId('__vocal-root__')).toBeInTheDocument()
24
- })
25
-
26
- it('renders custom children element', () => {
27
- const { queryByTestId } = render(getInstance(null, <div data-testid="__vocal-custom-root__" />))
28
- expect(queryByTestId('__vocal-root__')).not.toBeInTheDocument()
29
- expect(queryByTestId('__vocal-custom-root__')).toBeInTheDocument()
30
- })
31
-
32
- it('renders no children element if SpeechRecognition is not supported', () => {
33
- vi.spyOn(SpeechRecognitionWrapper, 'isSupported', 'get').mockReturnValueOnce(false)
34
- const { queryByTestId } = render(getInstance(null, <div data-testid="__vocal-custom-root__" />))
35
- expect(queryByTestId('__vocal-root__')).not.toBeInTheDocument()
36
- expect(queryByTestId('__vocal-custom-root__')).not.toBeInTheDocument()
37
- vi.clearAllMocks()
38
- })
39
-
40
- it('renders custom children function', () => {
41
- const { queryByTestId } = render(getInstance(null, () => <div data-testid="__vocal-custom-root__" />))
42
- expect(queryByTestId('__vocal-root__')).not.toBeInTheDocument()
43
- expect(queryByTestId('__vocal-custom-root__')).toBeInTheDocument()
44
- })
45
-
46
- it('starts recognition with custom children function', async () => {
47
- const onStart = vi.fn()
48
- const { queryByTestId } = render(
49
- getInstance({ onStart }, (start) => <div data-testid="__vocal-custom-root__" onClick={start} />)
50
- )
51
- await act(async () => {
52
- fireEvent.click(queryByTestId('__vocal-custom-root__'))
53
- await waitFor(() => expect(onStart).toHaveBeenCalled())
54
- })
55
- })
56
-
57
- it('stops recognition with custom children function', async () => {
58
- const onEnd = vi.fn()
59
- const { queryByText } = render(
60
- getInstance({ onEnd }, (start, stop) => (
61
- <div data-testid="__vocal-custom-root__">
62
- <button onClick={start}>start</button>
63
- <button onClick={stop}>stop</button>
64
- </div>
65
- ))
66
- )
67
- await act(async () => {
68
- fireEvent.click(queryByText('start'))
69
- fireEvent.click(queryByText('stop'))
70
- await waitFor(() => expect(onEnd).toHaveBeenCalled())
71
- })
72
- })
73
-
74
- it('gets recognition status with custom children function', async () => {
75
- const onEnd = vi.fn()
76
- const { queryByText } = render(
77
- getInstance({ onEnd }, (start, stop, isStarted) => (
78
- <div data-testid="__vocal-custom-root__">
79
- <div>{isStarted ? 'Started' : 'Stopped'}</div>
80
- <button onClick={start}>start</button>
81
- <button onClick={stop}>stop</button>
82
- </div>
83
- ))
84
- )
85
- await act(async () => {
86
- fireEvent.click(queryByText('start'))
87
- await waitFor(() => {
88
- expect(queryByText('Started')).toBeInTheDocument()
89
- })
90
- fireEvent.click(queryByText('stop'))
91
- await waitFor(() => {
92
- expect(queryByText('Stopped')).toBeInTheDocument()
93
- })
94
- })
95
- })
96
-
97
- it('renders pointer cursor when idle', () => {
98
- const { getByTestId } = render(getInstance())
99
- expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'pointer' })
100
- })
101
-
102
- it('renders default cursor when listening', () => {
103
- const { getByTestId } = render(getInstance())
104
- fireEvent.click(getByTestId('__vocal-root__'))
105
- expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'default' })
106
- })
107
-
108
- it('renders outline when focused', () => {
109
- const { getByTestId } = render(getInstance())
110
- fireEvent.focus(getByTestId('__vocal-root__'))
111
- expect(getByTestId('__vocal-root__')).toHaveStyle({ outline: '2px solid' })
112
- })
113
-
114
- it('remove outline when blurred', () => {
115
- const { getByTestId } = render(getInstance())
116
- fireEvent.blur(getByTestId('__vocal-root__'))
117
- expect(getByTestId('__vocal-root__')).toHaveStyle({ outline: 'none' })
118
- })
119
-
120
- it('not uses style when className is set', () => {
121
- const { getByTestId } = render(getInstance({ className: 'foo' }))
122
- expect(getByTestId('__vocal-root__')).not.toHaveStyle({ cursor: 'pointer' })
123
- })
124
-
125
- it('uses custom styles', () => {
126
- const { getByTestId } = render(getInstance({ style: { backgroundColor: 'blue' } }))
127
- // jest-dom v6 + jsdom 29: `div.style.color = 'blue'` reads back as 'blue' but getComputedStyle returns RGB; normalisation no longer bridges the gap
128
- expect(getByTestId('__vocal-root__')).toHaveStyle({ backgroundColor: 'rgb(0, 0, 255)' })
129
- })
130
-
131
- it('responds to command', async () => {
132
- const callback = vi.fn()
133
- const recognition = new SpeechRecognitionWrapper()
134
- const commands = { foo: callback }
135
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands }))
136
-
137
- let flag = false
138
- recognition.addEventListener('start', async () => {
139
- flag = true
140
- })
141
-
142
- await act(async () => {
143
- fireEvent.click(getByTestId('__vocal-root__'))
144
-
145
- await waitFor(() => flag)
146
-
147
- recognition.instance.say('Foo')
148
- await waitFor(() => expect(callback).toHaveBeenCalledWith('Foo'))
149
- })
150
- })
151
-
152
- it('triggers onStart handler', async () => {
153
- const onStart = vi.fn()
154
- const { queryByTestId } = render(getInstance({ onStart }))
155
- await act(async () => {
156
- fireEvent.click(queryByTestId('__vocal-root__'))
157
- await waitFor(() => expect(onStart).toHaveBeenCalled())
158
- })
159
- })
160
-
161
- it('triggers onResult handler', async () => {
162
- const onResult = vi.fn()
163
- const recognition = new SpeechRecognitionWrapper()
164
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onResult }))
165
-
166
- let flag = false
167
- recognition.addEventListener('start', async () => {
168
- flag = true
169
- })
170
-
171
- await act(async () => {
172
- fireEvent.click(getByTestId('__vocal-root__'))
173
-
174
- await waitFor(() => flag)
175
-
176
- recognition.instance.say('Foo')
177
- await waitFor(() => expect(onResult).toHaveBeenCalledWith('Foo', expect.anything()))
178
- })
179
- })
180
-
181
- it('triggers onNoMatch handler', async () => {
182
- const onNoMatch = vi.fn()
183
- const recognition = new SpeechRecognitionWrapper()
184
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onNoMatch }))
185
-
186
- let flag = false
187
- recognition.addEventListener('start', async () => {
188
- flag = true
189
- })
190
-
191
- await act(async () => {
192
- fireEvent.click(getByTestId('__vocal-root__'))
193
-
194
- await waitFor(() => flag)
195
-
196
- recognition.instance.say(null)
197
- await waitFor(() => expect(onNoMatch).toHaveBeenCalled())
198
- })
199
- })
200
-
201
- it('triggers onSpeechStart handler', async () => {
202
- const onSpeechStart = vi.fn()
203
- const recognition = new SpeechRecognitionWrapper()
204
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onSpeechStart }))
205
-
206
- let flag = false
207
- recognition.addEventListener('start', async () => {
208
- flag = true
209
- })
210
-
211
- await act(async () => {
212
- fireEvent.click(getByTestId('__vocal-root__'))
213
-
214
- await waitFor(() => flag)
215
-
216
- recognition.instance.say('Foo')
217
- await waitFor(() => expect(onSpeechStart).toHaveBeenCalled())
218
- })
219
- })
220
-
221
- it('triggers onSpeechEnd handler', async () => {
222
- const onSpeechEnd = vi.fn()
223
- const recognition = new SpeechRecognitionWrapper()
224
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onSpeechEnd }))
225
-
226
- let flag = false
227
- recognition.addEventListener('start', async () => {
228
- flag = true
229
- })
230
-
231
- await act(async () => {
232
- fireEvent.click(getByTestId('__vocal-root__'))
233
-
234
- await waitFor(() => flag)
235
-
236
- recognition.instance.say('Foo')
237
- await waitFor(() => expect(onSpeechEnd).toHaveBeenCalled())
238
- })
239
- })
240
-
241
- it('triggers onEnd handler after timeout', async () => {
242
- const timeout = 100
243
- const onEnd = vi.fn()
244
- const { getByTestId } = render(getInstance({ timeout, onEnd }))
245
- await act(async () => {
246
- fireEvent.click(getByTestId('__vocal-root__'))
247
- await waitFor(() => expect(onEnd).toHaveBeenCalled(), { timeout: timeout * 2 })
248
- })
249
- })
250
-
251
- it('triggers onEnd handler after speech', async () => {
252
- const onEnd = vi.fn()
253
- const recognition = new SpeechRecognitionWrapper()
254
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onEnd }))
255
-
256
- let flag = false
257
- recognition.addEventListener('start', async () => {
258
- flag = true
259
- })
260
-
261
- await act(async () => {
262
- fireEvent.click(getByTestId('__vocal-root__'))
263
-
264
- await waitFor(() => flag)
265
-
266
- recognition.instance.say('Foo')
267
- await waitFor(() => expect(onEnd).toHaveBeenCalled())
268
- })
269
- })
270
-
271
- it('calls the updated onEnd prop after a re-render during an active session', async () => {
272
- const onEndV1 = vi.fn()
273
- const onEndV2 = vi.fn()
274
- const recognition = new SpeechRecognitionWrapper()
275
- const { getByTestId, rerender } = render(getInstance({ __rsInstance: recognition, onEnd: onEndV1 }))
276
-
277
- await act(async () => {
278
- fireEvent.click(getByTestId('__vocal-root__'))
279
- await waitFor(() => expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'default' }))
280
- })
281
-
282
- await act(async () => {
283
- rerender(getInstance({ __rsInstance: recognition, onEnd: onEndV2 }))
284
- })
285
-
286
- await act(async () => {
287
- recognition.instance.say('Foo')
288
- await waitFor(() => expect(onEndV2).toHaveBeenCalled())
289
- })
290
-
291
- expect(onEndV1).not.toHaveBeenCalled()
292
- })
293
-
294
- it('resets to idle after a re-render during an active session', async () => {
295
- const onEnd = vi.fn()
296
- const recognition = new SpeechRecognitionWrapper()
297
- const { getByTestId, rerender } = render(getInstance({ __rsInstance: recognition, onEnd }))
298
-
299
- await act(async () => {
300
- fireEvent.click(getByTestId('__vocal-root__'))
301
- await waitFor(() => expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'default' }))
302
- })
303
-
304
- await act(async () => {
305
- rerender(getInstance({ __rsInstance: recognition, onEnd }))
306
- })
307
-
308
- await act(async () => {
309
- recognition.instance.say('Foo')
310
- await waitFor(() => expect(onEnd).toHaveBeenCalled())
311
- })
312
-
313
- expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'pointer' })
314
- })
315
-
316
- it('calls the updated onResult prop after a re-render during an active session', async () => {
317
- const onResultV1 = vi.fn()
318
- const onResultV2 = vi.fn()
319
- const recognition = new SpeechRecognitionWrapper()
320
- const { getByTestId, rerender } = render(getInstance({ __rsInstance: recognition, onResult: onResultV1 }))
321
-
322
- await act(async () => {
323
- fireEvent.click(getByTestId('__vocal-root__'))
324
- await waitFor(() => expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'default' }))
325
- })
326
-
327
- await act(async () => {
328
- rerender(getInstance({ __rsInstance: recognition, onResult: onResultV2 }))
329
- })
330
-
331
- await act(async () => {
332
- recognition.instance.say('Foo')
333
- await waitFor(() => expect(onResultV2).toHaveBeenCalledWith('Foo', expect.anything()))
334
- })
335
-
336
- expect(onResultV1).not.toHaveBeenCalled()
337
- })
338
-
339
- it('calls the updated onSpeechStart prop after a re-render during an active session', async () => {
340
- const onSpeechStartV1 = vi.fn()
341
- const onSpeechStartV2 = vi.fn()
342
- const recognition = new SpeechRecognitionWrapper()
343
- const { getByTestId, rerender } = render(getInstance({ __rsInstance: recognition, onSpeechStart: onSpeechStartV1 }))
344
-
345
- await act(async () => {
346
- fireEvent.click(getByTestId('__vocal-root__'))
347
- await waitFor(() => expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'default' }))
348
- })
349
-
350
- await act(async () => {
351
- rerender(getInstance({ __rsInstance: recognition, onSpeechStart: onSpeechStartV2 }))
352
- })
353
-
354
- await act(async () => {
355
- recognition.instance.say('Foo')
356
- await waitFor(() => expect(onSpeechStartV2).toHaveBeenCalled())
357
- })
358
-
359
- expect(onSpeechStartV1).not.toHaveBeenCalled()
360
- })
361
-
362
- it('calls the updated onSpeechEnd prop after a re-render during an active session', async () => {
363
- const onSpeechEndV1 = vi.fn()
364
- const onSpeechEndV2 = vi.fn()
365
- const recognition = new SpeechRecognitionWrapper()
366
- const { getByTestId, rerender } = render(getInstance({ __rsInstance: recognition, onSpeechEnd: onSpeechEndV1 }))
367
-
368
- await act(async () => {
369
- fireEvent.click(getByTestId('__vocal-root__'))
370
- await waitFor(() => expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'default' }))
371
- })
372
-
373
- await act(async () => {
374
- rerender(getInstance({ __rsInstance: recognition, onSpeechEnd: onSpeechEndV2 }))
375
- })
376
-
377
- await act(async () => {
378
- recognition.instance.say('Foo')
379
- await waitFor(() => expect(onSpeechEndV2).toHaveBeenCalled())
380
- })
381
-
382
- expect(onSpeechEndV1).not.toHaveBeenCalled()
383
- })
384
-
385
- it('calls the updated onNoMatch prop after a re-render during an active session', async () => {
386
- const onNoMatchV1 = vi.fn()
387
- const onNoMatchV2 = vi.fn()
388
- const recognition = new SpeechRecognitionWrapper()
389
- const { getByTestId, rerender } = render(getInstance({ __rsInstance: recognition, onNoMatch: onNoMatchV1 }))
390
-
391
- await act(async () => {
392
- fireEvent.click(getByTestId('__vocal-root__'))
393
- await waitFor(() => expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'default' }))
394
- })
395
-
396
- await act(async () => {
397
- rerender(getInstance({ __rsInstance: recognition, onNoMatch: onNoMatchV2 }))
398
- })
399
-
400
- await act(async () => {
401
- recognition.instance.say(null)
402
- await waitFor(() => expect(onNoMatchV2).toHaveBeenCalled())
403
- })
404
-
405
- expect(onNoMatchV1).not.toHaveBeenCalled()
406
- })
407
-
408
- it('calls the updated onError prop after a re-render during an active session', async () => {
409
- const onErrorV1 = vi.fn()
410
- const onErrorV2 = vi.fn()
411
- const recognition = new SpeechRecognitionWrapper()
412
- const { getByTestId, rerender } = render(getInstance({ __rsInstance: recognition, onError: onErrorV1 }))
413
-
414
- await act(async () => {
415
- fireEvent.click(getByTestId('__vocal-root__'))
416
- await waitFor(() => expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'default' }))
417
- })
418
-
419
- await act(async () => {
420
- rerender(getInstance({ __rsInstance: recognition, onError: onErrorV2 }))
421
- })
422
-
423
- await act(async () => {
424
- recognition.instance.error(new Error('mic failure'))
425
- await waitFor(() => expect(onErrorV2).toHaveBeenCalled())
426
- })
427
-
428
- expect(onErrorV1).not.toHaveBeenCalled()
429
- })
430
-
431
- it('triggers command matched on first segment in multi-segment result', async () => {
432
- const callback = vi.fn()
433
- const recognition = new SpeechRecognitionWrapper()
434
- const commands = { hello: callback }
435
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands }))
436
-
437
- await act(async () => {
438
- fireEvent.click(getByTestId('__vocal-root__'))
439
- recognition.instance.say([
440
- [{ transcript: 'hello', confidence: 0.9 }],
441
- [{ transcript: 'world', confidence: 0.8 }],
442
- ])
443
- await waitFor(() => expect(callback).toHaveBeenCalledWith('hello'))
444
- })
445
- })
446
-
447
- it('triggers command matched on second segment in multi-segment result', async () => {
448
- const callback = vi.fn()
449
- const recognition = new SpeechRecognitionWrapper()
450
- const commands = { world: callback }
451
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands }))
452
-
453
- await act(async () => {
454
- fireEvent.click(getByTestId('__vocal-root__'))
455
- recognition.instance.say([
456
- [{ transcript: 'hello', confidence: 0.9 }],
457
- [{ transcript: 'world', confidence: 0.8 }],
458
- ])
459
- await waitFor(() => expect(callback).toHaveBeenCalledWith('world'))
460
- })
461
- })
462
-
463
- it('does not trigger command when no segment matches', async () => {
464
- const callback = vi.fn()
465
- const recognition = new SpeechRecognitionWrapper()
466
- const commands = { foo: callback }
467
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands }))
468
-
469
- await act(async () => {
470
- fireEvent.click(getByTestId('__vocal-root__'))
471
- recognition.instance.say([
472
- [{ transcript: 'hello', confidence: 0.9 }],
473
- [{ transcript: 'world', confidence: 0.8 }],
474
- ])
475
- await new Promise((r) => setTimeout(r, 100))
476
- })
477
-
478
- expect(callback).not.toHaveBeenCalled()
479
- })
480
-
481
- it('fires only the first matching command when multiple segments each match a different command', async () => {
482
- const callbackHello = vi.fn()
483
- const callbackWorld = vi.fn()
484
- const recognition = new SpeechRecognitionWrapper()
485
- const commands = { hello: callbackHello, world: callbackWorld }
486
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands }))
487
-
488
- await act(async () => {
489
- fireEvent.click(getByTestId('__vocal-root__'))
490
- recognition.instance.say([
491
- [{ transcript: 'hello', confidence: 0.9 }],
492
- [{ transcript: 'world', confidence: 0.8 }],
493
- ])
494
- await waitFor(() => expect(callbackHello).toHaveBeenCalledWith('hello'))
495
- })
496
-
497
- expect(callbackWorld).not.toHaveBeenCalled()
498
- })
499
-
500
- it('passes full joined transcript to onResult regardless of command segment matching', async () => {
501
- const onResult = vi.fn()
502
- const recognition = new SpeechRecognitionWrapper()
503
- const commands = { hello: vi.fn() }
504
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands, onResult }))
505
-
506
- await act(async () => {
507
- fireEvent.click(getByTestId('__vocal-root__'))
508
- recognition.instance.say([
509
- [{ transcript: 'hello ', confidence: 0.9 }],
510
- [{ transcript: 'world', confidence: 0.8 }],
511
- ])
512
- await waitFor(() => expect(onResult).toHaveBeenCalledWith('hello world', expect.anything()))
513
- })
514
- })
515
-
516
- it('returns the most confident alternative as the onResult transcript', async () => {
517
- const onResult = vi.fn()
518
- const recognition = new SpeechRecognitionWrapper()
519
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onResult }))
520
-
521
- await act(async () => {
522
- fireEvent.click(getByTestId('__vocal-root__'))
523
- recognition.instance.say([[
524
- { transcript: 'bar', confidence: 0.4 },
525
- { transcript: 'foo', confidence: 0.9 },
526
- { transcript: 'baz', confidence: 0.1 },
527
- ]])
528
- await waitFor(() => expect(onResult).toHaveBeenCalledWith('foo', expect.anything()))
529
- })
530
- })
531
-
532
- it('joins all segments into the onResult transcript', async () => {
533
- const onResult = vi.fn()
534
- const recognition = new SpeechRecognitionWrapper()
535
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onResult }))
536
-
537
- await act(async () => {
538
- fireEvent.click(getByTestId('__vocal-root__'))
539
- recognition.instance.say([
540
- [{ transcript: 'hello ', confidence: 0.9 }],
541
- [{ transcript: 'world', confidence: 0.8 }],
542
- ])
543
- await waitFor(() => expect(onResult).toHaveBeenCalledWith('hello world', expect.anything()))
544
- })
545
- })
546
-
547
- it('triggers command matched on a word within a multi-word segment', async () => {
548
- const callback = vi.fn()
549
- const recognition = new SpeechRecognitionWrapper()
550
- const commands = { rouge: callback }
551
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands }))
552
-
553
- await act(async () => {
554
- fireEvent.click(getByTestId('__vocal-root__'))
555
- recognition.instance.say([[{ transcript: 'je veux du rouge', confidence: 0.9 }]])
556
- await waitFor(() => expect(callback).toHaveBeenCalledWith('rouge'))
557
- })
558
- })
559
-
560
- it('triggers command matched on a secondary alternative (homophone)', async () => {
561
- const callback = vi.fn()
562
- const recognition = new SpeechRecognitionWrapper()
563
- const commands = { vert: callback }
564
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands, maxAlternatives: 3 }))
565
-
566
- await act(async () => {
567
- fireEvent.click(getByTestId('__vocal-root__'))
568
- // Primary alternative is the homophone; secondary is the correct word
569
- recognition.instance.say([[
570
- { transcript: 'verre', confidence: 0.9 },
571
- { transcript: 'vert', confidence: 0.7 },
572
- ]])
573
- await waitFor(() => expect(callback).toHaveBeenCalledWith('vert'))
574
- })
575
- })
576
-
577
- it('passes the most confident transcript to onResult even when command matches a secondary alternative', async () => {
578
- const onResult = vi.fn()
579
- const recognition = new SpeechRecognitionWrapper()
580
- const commands = { vert: vi.fn() }
581
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands, onResult, maxAlternatives: 3 }))
582
-
583
- await act(async () => {
584
- fireEvent.click(getByTestId('__vocal-root__'))
585
- recognition.instance.say([[
586
- { transcript: 'verre', confidence: 0.9 },
587
- { transcript: 'vert', confidence: 0.7 },
588
- ]])
589
- await waitFor(() => expect(onResult).toHaveBeenCalledWith('verre', expect.anything()))
590
- })
591
- })
592
-
593
- it('calls onEnd via the end event when stop is asynchronous', async () => {
594
- const onEnd = vi.fn()
595
- const recognition = new SpeechRecognitionWrapper()
596
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onEnd }))
597
-
598
- // Simulate async stop: override stop() so the end event does not fire immediately
599
- recognition.instance.stop = vi.fn()
600
-
601
- await act(async () => {
602
- fireEvent.click(getByTestId('__vocal-root__'))
603
- recognition.instance.say('Foo')
604
- // stopRecognition was called but end has not fired yet — onEnd must not be called
605
- expect(onEnd).not.toHaveBeenCalled()
606
- // Browser fires end asynchronously after recognition stops
607
- recognition.instance.end()
608
- await waitFor(() => expect(onEnd).toHaveBeenCalled())
609
- })
610
- })
611
- })
@@ -1,38 +0,0 @@
1
- import React from 'react'
2
- import { waitFor } from '@testing-library/dom'
3
- import { act, fireEvent, render } from '@testing-library/react'
4
-
5
- import Vocal from '../Vocal'
6
-
7
- vi.mock('../../hooks/useVocal', () => {
8
- return {
9
- default: () => [
10
- null,
11
- {
12
- subscribe: () => {
13
- throw new Error('Foo')
14
- },
15
- },
16
- ],
17
- }
18
- })
19
-
20
- const defaultProps = {}
21
- const getInstance = (props = {}, children = null) => (
22
- <Vocal {...defaultProps} {...props}>
23
- {children}
24
- </Vocal>
25
- )
26
-
27
- describe('Vocal', () => {
28
- it('triggers onError handler', async () => {
29
- const onError = vi.fn()
30
- const { queryByTestId } = render(getInstance({ onError }))
31
- await act(async () => {
32
- fireEvent.click(queryByTestId('__vocal-root__'))
33
- await waitFor(() => expect(onError).toHaveBeenCalled())
34
- })
35
- })
36
- })
37
-
38
- // TODO: Merge this file with Vocal.test.js