@untemps/react-vocal 1.7.37 → 2.0.0-beta.10

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.
@@ -1,72 +0,0 @@
1
- import '@testing-library/jest-dom/extend-expect'
2
- import { toBeInTheDocument, toHaveAttribute, toHaveStyle } from '@testing-library/jest-dom/matchers'
3
-
4
- expect.extend({ toBeInTheDocument, toHaveAttribute, toHaveStyle })
5
-
6
- Object.defineProperty(global, 'navigator', {
7
- value: { userAgent: 'node.js' },
8
- writable: true,
9
- configurable: true,
10
- })
11
- global.PermissionStatus = jest.fn(() => ({
12
- state: 'granted',
13
- addEventListener: jest.fn(),
14
- }))
15
- const status = new PermissionStatus()
16
- global.Permissions = jest.fn(() => ({
17
- query: jest.fn().mockResolvedValue(status),
18
- }))
19
- Object.defineProperty(global.navigator, 'permissions', {
20
- value: new Permissions(),
21
- writable: true,
22
- configurable: true,
23
- })
24
- global.MediaDevices = jest.fn(() => ({
25
- getUserMedia: jest.fn().mockResolvedValue('foo'),
26
- }))
27
- Object.defineProperty(global.navigator, 'mediaDevices', {
28
- value: new MediaDevices(),
29
- writable: true,
30
- configurable: true,
31
- })
32
- global.SpeechGrammarList = jest.fn(() => ({
33
- length: 0,
34
- }))
35
- global.SpeechRecognition = jest.fn(() => {
36
- const handlers = {}
37
- return {
38
- addEventListener: jest.fn((type, callback) => {
39
- handlers[type] = callback
40
- }),
41
- removeEventListener: jest.fn(),
42
- dispatchEvent: jest.fn(),
43
- start: jest.fn(() => {
44
- !!handlers.start && handlers.start()
45
- }),
46
- stop: jest.fn(() => {
47
- !!handlers.end && handlers.end()
48
- }),
49
- abort: jest.fn(() => {
50
- !!handlers.end && handlers.end()
51
- }),
52
- say: jest.fn((sentence) => {
53
- !!handlers.speechstart && handlers.speechstart()
54
-
55
- const resultEvent = new Event('result')
56
- resultEvent.resultIndex = 0
57
- resultEvent.results = [
58
- [
59
- {
60
- transcript: sentence,
61
- },
62
- ],
63
- ]
64
- if (sentence) {
65
- !!handlers.result && handlers.result(resultEvent)
66
- } else {
67
- !!handlers.nomatch && handlers.nomatch()
68
- }
69
- !!handlers.speechend && handlers.speechend()
70
- }),
71
- }
72
- })
package/rollup.config.js DELETED
@@ -1,42 +0,0 @@
1
- import babel from '@rollup/plugin-babel'
2
- import commonjs from '@rollup/plugin-commonjs'
3
- import resolve from '@rollup/plugin-node-resolve'
4
- import filesize from 'rollup-plugin-sizes'
5
- import { terser } from 'rollup-plugin-terser'
6
- import visualizer from 'rollup-plugin-visualizer'
7
-
8
- const production = process.env.NODE_ENV === 'production'
9
- const target = process.env.BABEL_ENV
10
-
11
- export default {
12
- input: 'src/index.js',
13
- output: {
14
- name: 'react-vocal',
15
- file: {
16
- cjs: 'dist/index.js',
17
- es: 'dist/index.es.js',
18
- umd: 'dist/index.umd.js',
19
- }[target],
20
- format: target,
21
- globals: {
22
- react: 'React',
23
- 'react-dom': 'ReactDOM',
24
- 'prop-types': 'PropTypes',
25
- },
26
- sourcemap: 'inline',
27
- },
28
- external: ['react', 'react-dom', 'prop-types', '@babel/plugin-transform-runtime'],
29
- plugins: [
30
- babel({
31
- exclude: 'node_modules/**',
32
- babelHelpers: 'bundled',
33
- }),
34
- resolve(),
35
- commonjs(),
36
- production && terser(),
37
- filesize(),
38
- visualizer({
39
- sourcemap: true
40
- })
41
- ],
42
- }
@@ -1,37 +0,0 @@
1
- import React from 'react'
2
- import PropTypes from 'prop-types'
3
-
4
- const Icon = ({ color, activeColor, isActive }) => {
5
- return (
6
- <svg
7
- data-testid="__icon-root__"
8
- xmlns="http://www.w3.org/2000/svg"
9
- width="100%"
10
- height="100%"
11
- viewBox="0 0 24 24"
12
- >
13
- <g>
14
- <path
15
- data-testid="__icon-path__"
16
- fill={color}
17
- d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"
18
- />
19
- {isActive && <circle data-testid="__icon-active__" fill={activeColor} cx="16" cy="4" r="4" />}
20
- </g>
21
- </svg>
22
- )
23
- }
24
-
25
- Icon.propTypes = {
26
- color: PropTypes.string,
27
- activeColor: PropTypes.string,
28
- isActive: PropTypes.bool,
29
- }
30
-
31
- Icon.defaultProps = {
32
- color: 'black',
33
- activeColor: 'red',
34
- isActive: false,
35
- }
36
-
37
- export default Icon
@@ -1,235 +0,0 @@
1
- import React, { cloneElement, isValidElement, useRef, useState } from 'react'
2
- import PropTypes from 'prop-types'
3
- import { Vocal as SpeechRecognitionWrapper } from '@untemps/vocal'
4
- import { isFunction } from '@untemps/utils/function/isFunction'
5
-
6
- import useVocal from '../hooks/useVocal'
7
- import useTimeout from '../hooks/useTimeout'
8
- import useCommands from '../hooks/useCommands'
9
-
10
- import Icon from './Icon'
11
-
12
- const Vocal = ({
13
- children,
14
- commands,
15
- lang,
16
- grammars,
17
- timeout,
18
- ariaLabel,
19
- style,
20
- className,
21
- outlineStyle,
22
- onStart,
23
- onEnd,
24
- onSpeechStart,
25
- onSpeechEnd,
26
- onResult,
27
- onError,
28
- onNoMatch,
29
- __rsInstance,
30
- }) => {
31
- const buttonRef = useRef(null)
32
- const [isListening, setIsListening] = useState(false)
33
-
34
- const [, { start, stop, subscribe, unsubscribe }] = useVocal(lang, grammars, __rsInstance)
35
- const triggerCommand = useCommands(commands)
36
-
37
- const _onEnd = (e) => {
38
- stopTimer()
39
- stopRecognition()
40
-
41
- unsubscribe('start', _onStart)
42
- unsubscribe('end', _onEnd)
43
- unsubscribe('speechstart', _onSpeechStart)
44
- unsubscribe('speechend', _onSpeechEnd)
45
- unsubscribe('result', _onResult)
46
- unsubscribe('error', _onError)
47
- unsubscribe('nomatch', _onNoMatch)
48
-
49
- !!onEnd && onEnd(e)
50
- }
51
-
52
- const [startTimer, stopTimer] = useTimeout(_onEnd, timeout)
53
-
54
- const startRecognition = () => {
55
- try {
56
- setIsListening(true)
57
-
58
- subscribe('start', _onStart)
59
- subscribe('end', _onEnd)
60
- subscribe('speechstart', _onSpeechStart)
61
- subscribe('speechend', _onSpeechEnd)
62
- subscribe('result', _onResult)
63
- subscribe('error', _onError)
64
- subscribe('nomatch', _onNoMatch)
65
-
66
- start()
67
- } catch (error) {
68
- _onError(error)
69
- }
70
- }
71
-
72
- const stopRecognition = () => {
73
- try {
74
- setIsListening(false)
75
-
76
- stop()
77
- } catch (error) {
78
- !!onError && onError(error)
79
- }
80
- }
81
-
82
- const _onClick = () => {
83
- startRecognition()
84
- }
85
-
86
- const _onFocus = () => {
87
- if (!className && outlineStyle) {
88
- buttonRef.current.style.outline = outlineStyle
89
- }
90
- }
91
-
92
- const _onBlur = () => {
93
- if (!className && outlineStyle) {
94
- buttonRef.current.style.outline = 'none'
95
- }
96
- }
97
-
98
- const _onStart = (e) => {
99
- startTimer()
100
-
101
- !!onStart && onStart(e)
102
- }
103
-
104
- const _onSpeechStart = (e) => {
105
- stopTimer()
106
-
107
- !!onSpeechStart && onSpeechStart(e)
108
- }
109
-
110
- const _onSpeechEnd = (e) => {
111
- startTimer()
112
-
113
- !!onSpeechEnd && onSpeechEnd(e)
114
- }
115
-
116
- const _onResult = (event, result) => {
117
- stopTimer()
118
- stopRecognition()
119
-
120
- triggerCommand(result)
121
-
122
- !!onResult && onResult(result, event)
123
- }
124
-
125
- const _onError = (error) => {
126
- stopRecognition()
127
-
128
- !!onError && onError(error)
129
- }
130
-
131
- const _onNoMatch = (e) => {
132
- stopTimer()
133
- stopRecognition()
134
-
135
- !!onNoMatch && onNoMatch(e)
136
- }
137
-
138
- const _renderDefault = () => (
139
- <button
140
- data-testid="__vocal-root__"
141
- ref={buttonRef}
142
- role="button"
143
- aria-label={ariaLabel}
144
- style={
145
- className
146
- ? null
147
- : {
148
- width: 24,
149
- height: 24,
150
- background: 'none',
151
- border: 'none',
152
- padding: 0,
153
- cursor: !isListening ? 'pointer' : 'default',
154
- ...style,
155
- }
156
- }
157
- className={className}
158
- onFocus={_onFocus}
159
- onBlur={_onBlur}
160
- onClick={_onClick}
161
- >
162
- <Icon isActive={isListening} iconColor="#aaa" />
163
- </button>
164
- )
165
-
166
- const _renderChildren = (children) => {
167
- if (SpeechRecognitionWrapper.isSupported) {
168
- if (isFunction(children)) {
169
- return children(startRecognition, stopRecognition, isListening)
170
- } else if (isValidElement(children)) {
171
- return cloneElement(children, {
172
- ...(!isListening && { onClick: _onClick }),
173
- })
174
- } else {
175
- return _renderDefault()
176
- }
177
- }
178
- return null
179
- }
180
-
181
- return _renderChildren(children)
182
- }
183
-
184
- Vocal.propTypes = {
185
- /** Defines callbacks to be triggered when keys are detected by the recognition */
186
- commands: PropTypes.objectOf(PropTypes.func),
187
- /** Defines the language understood by the recognition (https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/lang) */
188
- lang: PropTypes.string,
189
- /** Defines the grammars understood by the recognition (https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/grammars) */
190
- grammars: PropTypes.object,
191
- /** Defines the time in ms to wait before discarding the recognition */
192
- timeout: PropTypes.number,
193
- /** Defines the a11y label for the default button */
194
- ariaLabel: PropTypes.string,
195
- /** Defines the styles of the default element if className is not specified */
196
- style: PropTypes.object,
197
- /** Defines the class of the default element */
198
- className: PropTypes.string,
199
- /** Defines the default style of the focus outline. if null the default behaviour is used */
200
- outlineStyle: PropTypes.string,
201
- /** Defines the handler called when the recognition starts */
202
- onStart: PropTypes.func,
203
- /** Defines the handler called when the recognition ends */
204
- onEnd: PropTypes.func,
205
- /** Defines the handler called when the speech starts */
206
- onSpeechStart: PropTypes.func,
207
- /** Defines the handler called when the speech ends */
208
- onSpeechEnd: PropTypes.func,
209
- /** Defines the handler called when a result is returned from te recognition */
210
- onResult: PropTypes.func,
211
- /** Defines the handler called when an error occurs */
212
- onError: PropTypes.func,
213
- /** Defines the handler called when no result can be recognized */
214
- onNoMatch: PropTypes.func,
215
- }
216
-
217
- Vocal.defaultProps = {
218
- commands: null,
219
- lang: 'en-US',
220
- grammars: null,
221
- timeout: 3000,
222
- ariaLabel: 'start recognition',
223
- style: null,
224
- className: null,
225
- outlineStyle: '2px solid',
226
- onStart: null,
227
- onEnd: null,
228
- onSpeechStart: null,
229
- onSpeechEnd: null,
230
- onResult: null,
231
- onError: null,
232
- onNoMatch: null,
233
- }
234
-
235
- export default Vocal
@@ -1,42 +0,0 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
4
-
5
- import React from 'react'
6
- import { render } from '@testing-library/react'
7
-
8
- import Icon from '../Icon'
9
-
10
- const defaultProps = {}
11
- const getInstance = (props = {}) => <Icon {...defaultProps} {...props} />
12
-
13
- describe('Icon', () => {
14
- it('matches snapshot', () => {
15
- const { asFragment } = render(getInstance())
16
- expect(asFragment()).toMatchSnapshot()
17
- })
18
-
19
- it('renders component', () => {
20
- const { queryByTestId } = render(getInstance())
21
- expect(queryByTestId('__icon-root__')).toBeInTheDocument()
22
- })
23
-
24
- it('renders component color', () => {
25
- const color = 'green'
26
- const { queryByTestId } = render(getInstance({ color }))
27
- expect(queryByTestId('__icon-path__')).toHaveAttribute('fill', color)
28
- })
29
-
30
- it('renders active component', () => {
31
- const isActive = true
32
- const { queryByTestId } = render(getInstance({ isActive }))
33
- expect(queryByTestId('__icon-active__')).toBeInTheDocument()
34
- })
35
-
36
- it('renders active component color', () => {
37
- const isActive = true
38
- const activeColor = 'blue'
39
- const { queryByTestId } = render(getInstance({ isActive, activeColor }))
40
- expect(queryByTestId('__icon-active__')).toHaveAttribute('fill', activeColor)
41
- })
42
- })
@@ -1,273 +0,0 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
4
-
5
- import React from 'react'
6
- import { waitFor } from '@testing-library/dom'
7
- import { act, fireEvent, render } from '@testing-library/react'
8
- import { Vocal as SpeechRecognitionWrapper } from '@untemps/vocal'
9
-
10
- import Vocal from '../Vocal'
11
-
12
- const defaultProps = {}
13
- const getInstance = (props = {}, children = null) => (
14
- <Vocal {...defaultProps} {...props}>
15
- {children}
16
- </Vocal>
17
- )
18
-
19
- describe('Vocal', () => {
20
- it('matches snapshot', () => {
21
- const { asFragment } = render(getInstance())
22
- expect(asFragment()).toMatchSnapshot()
23
- })
24
-
25
- it('renders default children', () => {
26
- const { queryByTestId } = render(getInstance())
27
- expect(queryByTestId('__vocal-root__')).toBeInTheDocument()
28
- })
29
-
30
- it('renders custom children element', () => {
31
- const { queryByTestId } = render(getInstance(null, <div data-testid="__vocal-custom-root__" />))
32
- expect(queryByTestId('__vocal-root__')).not.toBeInTheDocument()
33
- expect(queryByTestId('__vocal-custom-root__')).toBeInTheDocument()
34
- })
35
-
36
- it('renders no children element if SpeechRecognition is not supported', () => {
37
- jest.spyOn(SpeechRecognitionWrapper, 'isSupported', 'get').mockReturnValueOnce(false)
38
- const { queryByTestId } = render(getInstance(null, <div data-testid="__vocal-custom-root__" />))
39
- expect(queryByTestId('__vocal-root__')).not.toBeInTheDocument()
40
- expect(queryByTestId('__vocal-custom-root__')).not.toBeInTheDocument()
41
- jest.clearAllMocks()
42
- })
43
-
44
- it('renders custom children function', () => {
45
- const { queryByTestId } = render(getInstance(null, () => <div data-testid="__vocal-custom-root__" />))
46
- expect(queryByTestId('__vocal-root__')).not.toBeInTheDocument()
47
- expect(queryByTestId('__vocal-custom-root__')).toBeInTheDocument()
48
- })
49
-
50
- it('starts recognition with custom children function', async () => {
51
- const onStart = jest.fn()
52
- const { queryByTestId } = render(
53
- getInstance({ onStart }, (start) => <div data-testid="__vocal-custom-root__" onClick={start} />)
54
- )
55
- await act(async () => {
56
- fireEvent.click(queryByTestId('__vocal-custom-root__'))
57
- await waitFor(() => expect(onStart).toHaveBeenCalled())
58
- })
59
- })
60
-
61
- it('stops recognition with custom children function', async () => {
62
- const onEnd = jest.fn()
63
- const { queryByText } = render(
64
- getInstance({ onEnd }, (start, stop) => (
65
- <div data-testid="__vocal-custom-root__">
66
- <button onClick={start}>start</button>
67
- <button onClick={stop}>stop</button>
68
- </div>
69
- ))
70
- )
71
- await act(async () => {
72
- fireEvent.click(queryByText('start'))
73
- fireEvent.click(queryByText('stop'))
74
- await waitFor(() => expect(onEnd).toHaveBeenCalled())
75
- })
76
- })
77
-
78
- it('gets recognition status with custom children function', async () => {
79
- const onEnd = jest.fn()
80
- const { queryByText } = render(
81
- getInstance({ onEnd }, (start, stop, isStarted) => (
82
- <div data-testid="__vocal-custom-root__">
83
- <div>{isStarted ? 'Started' : 'Stopped'}</div>
84
- <button onClick={start}>start</button>
85
- <button onClick={stop}>stop</button>
86
- </div>
87
- ))
88
- )
89
- await act(async () => {
90
- fireEvent.click(queryByText('start'))
91
- await waitFor(() => {
92
- expect(queryByText('Started')).toBeInTheDocument()
93
- })
94
- fireEvent.click(queryByText('stop'))
95
- await waitFor(() => {
96
- expect(queryByText('Stopped')).toBeInTheDocument()
97
- })
98
- })
99
- })
100
-
101
- it('renders pointer cursor when idle', () => {
102
- const { getByTestId } = render(getInstance())
103
- expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'pointer' })
104
- })
105
-
106
- it('renders default cursor when listening', () => {
107
- const { getByTestId } = render(getInstance())
108
- fireEvent.click(getByTestId('__vocal-root__'))
109
- expect(getByTestId('__vocal-root__')).toHaveStyle({ cursor: 'default' })
110
- })
111
-
112
- it('renders outline when focused', () => {
113
- const { getByTestId } = render(getInstance())
114
- fireEvent.focus(getByTestId('__vocal-root__'))
115
- expect(getByTestId('__vocal-root__')).toHaveStyle({ outline: '2px solid' })
116
- })
117
-
118
- it('remove outline when blurred', () => {
119
- const { getByTestId } = render(getInstance())
120
- fireEvent.blur(getByTestId('__vocal-root__'))
121
- expect(getByTestId('__vocal-root__')).toHaveStyle({ outline: 'none' })
122
- })
123
-
124
- it('not uses style when className is set', () => {
125
- const { getByTestId } = render(getInstance({ className: 'foo' }))
126
- expect(getByTestId('__vocal-root__')).not.toHaveStyle({ cursor: 'pointer' })
127
- })
128
-
129
- it('uses custom styles', () => {
130
- const { getByTestId } = render(getInstance({ style: { backgroundColor: 'blue' } }))
131
- expect(getByTestId('__vocal-root__')).toHaveStyle({ backgroundColor: 'blue' })
132
- })
133
-
134
- it('responds to command', async () => {
135
- const callback = jest.fn()
136
- const recognition = new SpeechRecognitionWrapper()
137
- const commands = { foo: callback }
138
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands }))
139
-
140
- let flag = false
141
- recognition.addEventListener('start', async () => {
142
- flag = true
143
- })
144
-
145
- await act(async () => {
146
- fireEvent.click(getByTestId('__vocal-root__'))
147
-
148
- await waitFor(() => flag)
149
-
150
- recognition.instance.say('Foo')
151
- await waitFor(() => expect(callback).toHaveBeenCalledWith('Foo'))
152
- })
153
- })
154
-
155
- it('triggers onStart handler', async () => {
156
- const onStart = jest.fn()
157
- const { queryByTestId } = render(getInstance({ onStart }))
158
- await act(async () => {
159
- fireEvent.click(queryByTestId('__vocal-root__'))
160
- await waitFor(() => expect(onStart).toHaveBeenCalled())
161
- })
162
- })
163
-
164
- it('triggers onResult handler', async () => {
165
- const onResult = jest.fn()
166
- const recognition = new SpeechRecognitionWrapper()
167
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onResult }))
168
-
169
- let flag = false
170
- recognition.addEventListener('start', async () => {
171
- flag = true
172
- })
173
-
174
- await act(async () => {
175
- fireEvent.click(getByTestId('__vocal-root__'))
176
-
177
- await waitFor(() => flag)
178
-
179
- recognition.instance.say('Foo')
180
- await waitFor(() => expect(onResult).toHaveBeenCalledWith('Foo', expect.anything()))
181
- })
182
- })
183
-
184
- it('triggers onNoMatch handler', async () => {
185
- const onNoMatch = jest.fn()
186
- const recognition = new SpeechRecognitionWrapper()
187
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onNoMatch }))
188
-
189
- let flag = false
190
- recognition.addEventListener('start', async () => {
191
- flag = true
192
- })
193
-
194
- await act(async () => {
195
- fireEvent.click(getByTestId('__vocal-root__'))
196
-
197
- await waitFor(() => flag)
198
-
199
- recognition.instance.say(null)
200
- await waitFor(() => expect(onNoMatch).toHaveBeenCalled())
201
- })
202
- })
203
-
204
- it('triggers onSpeechStart handler', async () => {
205
- const onSpeechStart = jest.fn()
206
- const recognition = new SpeechRecognitionWrapper()
207
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onSpeechStart }))
208
-
209
- let flag = false
210
- recognition.addEventListener('start', async () => {
211
- flag = true
212
- })
213
-
214
- await act(async () => {
215
- fireEvent.click(getByTestId('__vocal-root__'))
216
-
217
- await waitFor(() => flag)
218
-
219
- recognition.instance.say('Foo')
220
- await waitFor(() => expect(onSpeechStart).toHaveBeenCalled())
221
- })
222
- })
223
-
224
- it('triggers onSpeechEnd handler', async () => {
225
- const onSpeechEnd = jest.fn()
226
- const recognition = new SpeechRecognitionWrapper()
227
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onSpeechEnd }))
228
-
229
- let flag = false
230
- recognition.addEventListener('start', async () => {
231
- flag = true
232
- })
233
-
234
- await act(async () => {
235
- fireEvent.click(getByTestId('__vocal-root__'))
236
-
237
- await waitFor(() => flag)
238
-
239
- recognition.instance.say('Foo')
240
- await waitFor(() => expect(onSpeechEnd).toHaveBeenCalled())
241
- })
242
- })
243
-
244
- it('triggers onEnd handler after timeout', async () => {
245
- const timeout = 100
246
- const onEnd = jest.fn()
247
- const { getByTestId } = render(getInstance({ timeout, onEnd }))
248
- await act(async () => {
249
- fireEvent.click(getByTestId('__vocal-root__'))
250
- await waitFor(() => expect(onEnd).toHaveBeenCalled(), { timeout: timeout * 2 })
251
- })
252
- })
253
-
254
- it('triggers onEnd handler after speech', async () => {
255
- const onEnd = jest.fn()
256
- const recognition = new SpeechRecognitionWrapper()
257
- const { getByTestId } = render(getInstance({ __rsInstance: recognition, onEnd }))
258
-
259
- let flag = false
260
- recognition.addEventListener('start', async () => {
261
- flag = true
262
- })
263
-
264
- await act(async () => {
265
- fireEvent.click(getByTestId('__vocal-root__'))
266
-
267
- await waitFor(() => flag)
268
-
269
- recognition.instance.say('Foo')
270
- await waitFor(() => expect(onEnd).toHaveBeenCalled())
271
- })
272
- })
273
- })