@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.
- package/CHANGELOG.md +98 -2
- package/README.md +72 -31
- package/dist/index.es.js +567 -2
- package/dist/index.js +7 -2
- package/package.json +39 -65
- package/.github/workflows/publish.yml +0 -30
- package/.prettierignore +0 -2
- package/.prettierrc +0 -29
- package/CLAUDE.md +0 -55
- package/assets/icon-idle.png +0 -0
- package/assets/icon-listening.png +0 -0
- package/assets/microphone.png +0 -0
- package/assets/react-vocal.png +0 -0
- package/babel.config.js +0 -12
- package/commitlint.config.js +0 -7
- package/dev/babel.config.js +0 -4
- package/dev/package.json +0 -14
- package/dev/public/index.html +0 -24
- package/dev/rollup.config.js +0 -29
- package/dev/src/index.js +0 -46
- package/dev/yarn.lock +0 -201
- package/dist/index.umd.js +0 -2
- package/jest/jest.setup.js +0 -72
- package/rollup.config.js +0 -42
- package/src/components/Icon.js +0 -37
- package/src/components/Vocal.js +0 -235
- package/src/components/__tests__/Icon.test.js +0 -42
- package/src/components/__tests__/Vocal.test.js +0 -273
- package/src/components/__tests__/VocalWithMockedUseVocal.test.js +0 -40
- package/src/hooks/__tests__/useCommands.test.js +0 -64
- package/src/hooks/__tests__/useTimeout.test.js +0 -69
- package/src/hooks/__tests__/useVocal.test.js +0 -197
- package/src/hooks/useCommands.js +0 -21
- package/src/hooks/useTimeout.js +0 -23
- package/src/hooks/useVocal.js +0 -58
- package/src/index.js +0 -7
package/jest/jest.setup.js
DELETED
|
@@ -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
|
-
}
|
package/src/components/Icon.js
DELETED
|
@@ -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
|
package/src/components/Vocal.js
DELETED
|
@@ -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
|
-
})
|