@untemps/react-vocal 2.0.0-beta.1 → 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 +78 -0
- package/README.md +54 -20
- package/dist/index.es.js +330 -1914
- package/dist/index.js +2 -4
- package/package.json +14 -7
- package/.github/workflows/publish.yml +0 -32
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -1
- package/.prettierignore +0 -3
- package/.prettierrc +0 -29
- package/CLAUDE.md +0 -59
- 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/commitlint.config.js +0 -7
- package/dev/index.html +0 -24
- package/dev/package.json +0 -18
- package/dev/public/index.html +0 -24
- package/dev/src/index.jsx +0 -45
- package/dev/vite.config.js +0 -10
- package/dev/yarn.lock +0 -201
- package/dist/index.es.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.umd.js +0 -9
- package/dist/index.umd.js.map +0 -1
- package/src/components/Icon.jsx +0 -24
- package/src/components/Vocal.jsx +0 -168
- package/src/components/__tests__/Icon.test.jsx +0 -38
- package/src/components/__tests__/Vocal.test.jsx +0 -270
- package/src/components/__tests__/VocalWithMockedUseVocal.test.jsx +0 -38
- package/src/components/__tests__/__snapshots__/Icon.test.jsx.snap +0 -21
- package/src/components/__tests__/__snapshots__/Vocal.test.jsx.snap +0 -28
- 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 -21
- package/src/hooks/useVocal.js +0 -56
- package/src/index.js +0 -7
- package/vite.config.js +0 -35
- package/vitest.setup.js +0 -71
package/src/components/Vocal.jsx
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import React, { cloneElement, isValidElement, useRef, useState } from 'react'
|
|
2
|
-
import { Vocal as SpeechRecognitionWrapper } from '@untemps/vocal'
|
|
3
|
-
import { isFunction } from '@untemps/utils/function/isFunction'
|
|
4
|
-
|
|
5
|
-
import useVocal from '../hooks/useVocal'
|
|
6
|
-
import useTimeout from '../hooks/useTimeout'
|
|
7
|
-
import useCommands from '../hooks/useCommands'
|
|
8
|
-
|
|
9
|
-
import Icon from './Icon'
|
|
10
|
-
|
|
11
|
-
const Vocal = ({
|
|
12
|
-
children,
|
|
13
|
-
commands = null,
|
|
14
|
-
lang = 'en-US',
|
|
15
|
-
grammars = null,
|
|
16
|
-
timeout = 3000,
|
|
17
|
-
ariaLabel = 'start recognition',
|
|
18
|
-
style = null,
|
|
19
|
-
className = null,
|
|
20
|
-
outlineStyle = '2px solid',
|
|
21
|
-
onStart = null,
|
|
22
|
-
onEnd = null,
|
|
23
|
-
onSpeechStart = null,
|
|
24
|
-
onSpeechEnd = null,
|
|
25
|
-
onResult = null,
|
|
26
|
-
onError = null,
|
|
27
|
-
onNoMatch = null,
|
|
28
|
-
__rsInstance,
|
|
29
|
-
}) => {
|
|
30
|
-
const buttonRef = useRef(null)
|
|
31
|
-
const [isListening, setIsListening] = useState(false)
|
|
32
|
-
|
|
33
|
-
const [, { start, stop, subscribe, unsubscribe }] = useVocal(lang, grammars, __rsInstance)
|
|
34
|
-
const triggerCommand = useCommands(commands)
|
|
35
|
-
|
|
36
|
-
const _onEnd = (e) => {
|
|
37
|
-
stopTimer()
|
|
38
|
-
stopRecognition()
|
|
39
|
-
unsubscribeAll()
|
|
40
|
-
onEnd?.(e)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const [startTimer, stopTimer] = useTimeout(_onEnd, timeout)
|
|
44
|
-
|
|
45
|
-
const startRecognition = () => {
|
|
46
|
-
try {
|
|
47
|
-
setIsListening(true)
|
|
48
|
-
subscribeAll()
|
|
49
|
-
start()
|
|
50
|
-
} catch (error) {
|
|
51
|
-
_onError(error)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const stopRecognition = () => {
|
|
56
|
-
try {
|
|
57
|
-
setIsListening(false)
|
|
58
|
-
stop()
|
|
59
|
-
} catch (error) {
|
|
60
|
-
onError?.(error)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const _onFocus = () => {
|
|
65
|
-
if (!className && outlineStyle) {
|
|
66
|
-
buttonRef.current.style.outline = outlineStyle
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const _onBlur = () => {
|
|
71
|
-
if (!className && outlineStyle) {
|
|
72
|
-
buttonRef.current.style.outline = 'none'
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const _onStart = (e) => {
|
|
77
|
-
startTimer()
|
|
78
|
-
onStart?.(e)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const _onSpeechStart = (e) => {
|
|
82
|
-
stopTimer()
|
|
83
|
-
onSpeechStart?.(e)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const _onSpeechEnd = (e) => {
|
|
87
|
-
startTimer()
|
|
88
|
-
onSpeechEnd?.(e)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const _onResult = (event, result) => {
|
|
92
|
-
stopTimer()
|
|
93
|
-
stopRecognition()
|
|
94
|
-
triggerCommand(result)
|
|
95
|
-
onResult?.(result, event)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const _onError = (error) => {
|
|
99
|
-
stopRecognition()
|
|
100
|
-
onError?.(error)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const _onNoMatch = (e) => {
|
|
104
|
-
stopTimer()
|
|
105
|
-
stopRecognition()
|
|
106
|
-
onNoMatch?.(e)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const HANDLERS = {
|
|
110
|
-
start: _onStart,
|
|
111
|
-
end: _onEnd,
|
|
112
|
-
speechstart: _onSpeechStart,
|
|
113
|
-
speechend: _onSpeechEnd,
|
|
114
|
-
result: _onResult,
|
|
115
|
-
error: _onError,
|
|
116
|
-
nomatch: _onNoMatch,
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const subscribeAll = () => Object.entries(HANDLERS).forEach(([event, handler]) => subscribe(event, handler))
|
|
120
|
-
const unsubscribeAll = () => Object.entries(HANDLERS).forEach(([event, handler]) => unsubscribe(event, handler))
|
|
121
|
-
|
|
122
|
-
const _renderDefault = () => (
|
|
123
|
-
<button
|
|
124
|
-
data-testid="__vocal-root__"
|
|
125
|
-
ref={buttonRef}
|
|
126
|
-
role="button"
|
|
127
|
-
aria-label={ariaLabel}
|
|
128
|
-
style={
|
|
129
|
-
className
|
|
130
|
-
? null
|
|
131
|
-
: {
|
|
132
|
-
width: 24,
|
|
133
|
-
height: 24,
|
|
134
|
-
backgroundColor: 'transparent', // `background: none` shorthand resets all sub-properties; jsdom 29 + jest-dom v6 don't reflect that correctly via getComputedStyle
|
|
135
|
-
border: 'none',
|
|
136
|
-
padding: 0,
|
|
137
|
-
cursor: !isListening ? 'pointer' : 'default',
|
|
138
|
-
...style,
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
className={className}
|
|
142
|
-
onFocus={_onFocus}
|
|
143
|
-
onBlur={_onBlur}
|
|
144
|
-
onClick={startRecognition}
|
|
145
|
-
>
|
|
146
|
-
<Icon isActive={isListening} color="#aaa" />
|
|
147
|
-
</button>
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
const _renderChildren = () => {
|
|
151
|
-
if (SpeechRecognitionWrapper.isSupported) {
|
|
152
|
-
if (isFunction(children)) {
|
|
153
|
-
return children(startRecognition, stopRecognition, isListening)
|
|
154
|
-
} else if (isValidElement(children)) {
|
|
155
|
-
return cloneElement(children, {
|
|
156
|
-
...(!isListening && { onClick: startRecognition }),
|
|
157
|
-
})
|
|
158
|
-
} else {
|
|
159
|
-
return _renderDefault()
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return null
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return _renderChildren()
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export default Vocal
|
|
@@ -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,270 +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
|
-
})
|
|
@@ -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
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`Icon > matches snapshot 1`] = `
|
|
4
|
-
<DocumentFragment>
|
|
5
|
-
<svg
|
|
6
|
-
data-testid="__icon-root__"
|
|
7
|
-
height="100%"
|
|
8
|
-
viewBox="0 0 24 24"
|
|
9
|
-
width="100%"
|
|
10
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
-
>
|
|
12
|
-
<g>
|
|
13
|
-
<path
|
|
14
|
-
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"
|
|
15
|
-
data-testid="__icon-path__"
|
|
16
|
-
fill="black"
|
|
17
|
-
/>
|
|
18
|
-
</g>
|
|
19
|
-
</svg>
|
|
20
|
-
</DocumentFragment>
|
|
21
|
-
`;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`Vocal > matches snapshot 1`] = `
|
|
4
|
-
<DocumentFragment>
|
|
5
|
-
<button
|
|
6
|
-
aria-label="start recognition"
|
|
7
|
-
data-testid="__vocal-root__"
|
|
8
|
-
role="button"
|
|
9
|
-
style="width: 24px; height: 24px; background-color: transparent; border: medium; padding: 0px; cursor: pointer;"
|
|
10
|
-
>
|
|
11
|
-
<svg
|
|
12
|
-
data-testid="__icon-root__"
|
|
13
|
-
height="100%"
|
|
14
|
-
viewBox="0 0 24 24"
|
|
15
|
-
width="100%"
|
|
16
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
17
|
-
>
|
|
18
|
-
<g>
|
|
19
|
-
<path
|
|
20
|
-
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"
|
|
21
|
-
data-testid="__icon-path__"
|
|
22
|
-
fill="#aaa"
|
|
23
|
-
/>
|
|
24
|
-
</g>
|
|
25
|
-
</svg>
|
|
26
|
-
</button>
|
|
27
|
-
</DocumentFragment>
|
|
28
|
-
`;
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { renderHook } from '@testing-library/react'
|
|
2
|
-
|
|
3
|
-
import useCommands from '../useCommands'
|
|
4
|
-
|
|
5
|
-
describe('useCommands', () => {
|
|
6
|
-
it('returns triggerCommand function', () => {
|
|
7
|
-
const triggerCommand = renderHook(() => useCommands())
|
|
8
|
-
expect(triggerCommand).toBeDefined()
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
it('triggers callback mapped to the exact input', () => {
|
|
12
|
-
const commands = {
|
|
13
|
-
foo: () => 'bar',
|
|
14
|
-
}
|
|
15
|
-
const {
|
|
16
|
-
result: { current: triggerCommand },
|
|
17
|
-
} = renderHook(() => useCommands(commands))
|
|
18
|
-
expect(triggerCommand('foo')).toBe('bar')
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('passes input as callback argument', () => {
|
|
22
|
-
const commands = {
|
|
23
|
-
foo: (input) => input,
|
|
24
|
-
}
|
|
25
|
-
const {
|
|
26
|
-
result: { current: triggerCommand },
|
|
27
|
-
} = renderHook(() => useCommands(commands))
|
|
28
|
-
expect(triggerCommand('foo')).toBe('foo')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
describe('Approximate inputs', () => {
|
|
32
|
-
const value = 'foo'
|
|
33
|
-
it.each([
|
|
34
|
-
['Change la bordure en vert', 'Change la bordure en verre', value],
|
|
35
|
-
['Change la bordure en vert', 'Change la bordure en verres', value],
|
|
36
|
-
['Change la bordure en vert', 'Change la bordure en vers', value],
|
|
37
|
-
['Change la bordure en vert', 'Change la bordure en vairs', value],
|
|
38
|
-
['Change la bordure en vert', 'Changez la bordure en verre', value],
|
|
39
|
-
['Change la bordure en vert', 'Changez la bodure en verre', null],
|
|
40
|
-
['Change la bordure en vert', 'Change la bordure en rouge', null],
|
|
41
|
-
['Change la bordure en vert', 'Change la bordure en verre de rouge', null],
|
|
42
|
-
['Change la bordure en vert', 'Change la bordure en violet', null],
|
|
43
|
-
['Change la bordure en vert', 'Modifie la bordure en violet', null],
|
|
44
|
-
])('triggers callback mapped to approximate inputs', (command, input, expected) => {
|
|
45
|
-
const commands = {
|
|
46
|
-
[command]: () => value,
|
|
47
|
-
}
|
|
48
|
-
const {
|
|
49
|
-
result: { current: triggerCommand },
|
|
50
|
-
} = renderHook(() => useCommands(commands))
|
|
51
|
-
expect(triggerCommand(input)).toBe(expected)
|
|
52
|
-
})
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('returns null as no command is mapped to the input', () => {
|
|
56
|
-
const commands = {
|
|
57
|
-
foo: () => 'bar',
|
|
58
|
-
}
|
|
59
|
-
const {
|
|
60
|
-
result: { current: triggerCommand },
|
|
61
|
-
} = renderHook(() => useCommands(commands))
|
|
62
|
-
expect(triggerCommand('gag')).toBeNull()
|
|
63
|
-
})
|
|
64
|
-
})
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { renderHook } from '@testing-library/react'
|
|
2
|
-
|
|
3
|
-
import useTimeout from '../useTimeout'
|
|
4
|
-
|
|
5
|
-
const wait = (delay) => {
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
setTimeout(resolve, delay)
|
|
8
|
-
})
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('useTimeout', () => {
|
|
12
|
-
it('not triggers handler before calling start', () => {
|
|
13
|
-
const handler = vi.fn()
|
|
14
|
-
renderHook(() => useTimeout(handler))
|
|
15
|
-
expect(handler).not.toHaveBeenCalled()
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('not triggers handler before timeout', async () => {
|
|
19
|
-
const handler = vi.fn()
|
|
20
|
-
const timeout = 500
|
|
21
|
-
const {
|
|
22
|
-
result: {
|
|
23
|
-
current: [start],
|
|
24
|
-
},
|
|
25
|
-
} = renderHook(() => useTimeout(handler, timeout))
|
|
26
|
-
start()
|
|
27
|
-
await wait(timeout - 50)
|
|
28
|
-
expect(handler).not.toHaveBeenCalled()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('triggers handler immediately', async () => {
|
|
32
|
-
const handler = vi.fn()
|
|
33
|
-
const {
|
|
34
|
-
result: {
|
|
35
|
-
current: [start],
|
|
36
|
-
},
|
|
37
|
-
} = renderHook(() => useTimeout(handler))
|
|
38
|
-
start()
|
|
39
|
-
await wait(0)
|
|
40
|
-
expect(handler).toHaveBeenCalled()
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('triggers handler after delay', async () => {
|
|
44
|
-
const handler = vi.fn()
|
|
45
|
-
const timeout = 500
|
|
46
|
-
const {
|
|
47
|
-
result: {
|
|
48
|
-
current: [start],
|
|
49
|
-
},
|
|
50
|
-
} = renderHook(() => useTimeout(handler, timeout))
|
|
51
|
-
start()
|
|
52
|
-
await wait(timeout)
|
|
53
|
-
expect(handler).toHaveBeenCalled()
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('not triggers handler if stop is called before timeout', async () => {
|
|
57
|
-
const handler = vi.fn()
|
|
58
|
-
const timeout = 500
|
|
59
|
-
const {
|
|
60
|
-
result: {
|
|
61
|
-
current: [start, stop],
|
|
62
|
-
},
|
|
63
|
-
} = renderHook(() => useTimeout(handler, timeout))
|
|
64
|
-
start()
|
|
65
|
-
stop()
|
|
66
|
-
await wait(timeout)
|
|
67
|
-
expect(handler).not.toHaveBeenCalled()
|
|
68
|
-
})
|
|
69
|
-
})
|