Skip to content

Commit

Permalink
feat: Add commands support (#58)
Browse files Browse the repository at this point in the history
* feat: Add support for commands
* refactor: Rename useCommands hook
* test: Add useCommands hook tests
* fix: Fix issue when undefined commands are passed to useCommands hook
* test: Add more tests for useCommands hook
* feat: Add precision parameter to useCommands hook
* test: Add more tests for useCommands hook and Vocal component
* docs(README): Add section for commands prop in README
* refactor: Fix commands prop type to fit expected shape
* refactor: Switch from `toLocaleLowerCase` to `toLowerCase` to reference command keys, #60
  • Loading branch information
untemps authored Jul 16, 2021
1 parent e1270c8 commit 12e086d
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 163 deletions.
374 changes: 215 additions & 159 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dev/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default {
},
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.NODE_ENV': JSON.stringify('development'),
}),
babel({
exclude: 'node_modules/**',
Expand Down
14 changes: 12 additions & 2 deletions dev/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Vocal from '../../src'

const App = () => {
const [logs, setLogs] = useState('')
const [borderColor, setBorderColor] = useState()

const _log = (value) => setLogs((logs) => `${logs}${logs.length > 0 ? '\n' : ''} ----- ${value}`)

Expand All @@ -27,8 +28,17 @@ const App = () => {

return (
<>
<Vocal onStart={_onVocalStart} onEnd={_onVocalEnd} onResult={_onVocalResult} onError={_onVocalError} />
<textarea value={logs} rows={30} disabled style={{ width: '100%', marginTop: 16 }} />
<Vocal
lang="fr"
commands={{
'Change la bordure en rouge': () => setBorderColor('red'),
}}
onStart={_onVocalStart}
onEnd={_onVocalEnd}
onResult={_onVocalResult}
onError={_onVocalError}
/>
<textarea value={logs} rows={30} disabled style={{ width: '100%', marginTop: 16, borderColor }} />
</>
)
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"react-dom": "^16.13.1"
},
"dependencies": {
"@untemps/vocal": "^1.3.0"
"@untemps/vocal": "^1.3.0",
"fuse.js": "^6.4.6"
},
"jest": {
"coverageDirectory": "./coverage/",
Expand Down
8 changes: 8 additions & 0 deletions src/components/Vocal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import isFunc from '../utils/isFunc'

import useVocal from '../hooks/useVocal'
import useTimeout from '../hooks/useTimeout'
import useCommands from '../hooks/useCommands'

import Icon from './Icon'

const Vocal = ({
children,
commands,
lang,
grammars,
timeout,
Expand All @@ -31,6 +33,7 @@ const Vocal = ({
const [isListening, setIsListening] = useState(false)

const [, { start, stop, subscribe, unsubscribe }] = useVocal(lang, grammars, __rsInstance)
const triggerCommand = useCommands(commands)

const _onEnd = (e) => {
stopTimer()
Expand Down Expand Up @@ -115,6 +118,8 @@ const Vocal = ({
stopTimer()
stopRecognition()

triggerCommand(result)

!!onResult && onResult(result, event)
}

Expand Down Expand Up @@ -178,6 +183,8 @@ const Vocal = ({
}

Vocal.propTypes = {
/** Defines callbacks to be triggered when keys are detected by the recognition */
commands: PropTypes.objectOf(PropTypes.func),
/** Defines the language understood by the recognition (https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/lang) */
lang: PropTypes.string,
/** Defines the grammars understood by the recognition (https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/grammars) */
Expand Down Expand Up @@ -209,6 +216,7 @@ Vocal.propTypes = {
}

Vocal.defaultProps = {
commands: null,
lang: 'en-US',
grammars: null,
timeout: 3000,
Expand Down
21 changes: 21 additions & 0 deletions src/components/__tests__/Vocal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,27 @@ describe('Vocal', () => {
expect(getByTestId('__vocal-root__')).toHaveStyle({ backgroundColor: 'blue' })
})

it('responds to command', async () => {
const callback = jest.fn()
const recognition = new SpeechRecognitionWrapper()
const commands = { foo: callback }
const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands }))

let flag = false
recognition.addEventListener('start', async () => {
flag = true
})

await act(async () => {
fireEvent.click(getByTestId('__vocal-root__'))

await waitFor(() => flag)

recognition.instance.say('Foo')
await waitFor(() => expect(callback).toHaveBeenCalledWith('Foo'))
})
})

it('triggers onStart handler', async () => {
const onStart = jest.fn()
const { queryByTestId } = render(getInstance({ onStart }))
Expand Down
64 changes: 64 additions & 0 deletions src/hooks/__tests__/useCommands.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { renderHook } from '@testing-library/react-hooks'

import useCommands from '../useCommands'

describe('useCommands', () => {
it('returns triggerCommand function', () => {
const triggerCommand = renderHook(() => useCommands())
expect(triggerCommand).toBeDefined()
})

it('triggers callback mapped to the exact input', () => {
const commands = {
foo: () => 'bar',
}
const {
result: { current: triggerCommand },
} = renderHook(() => useCommands(commands))
expect(triggerCommand('foo')).toBe('bar')
})

it('passes input as callback argument', () => {
const commands = {
foo: (input) => input,
}
const {
result: { current: triggerCommand },
} = renderHook(() => useCommands(commands))
expect(triggerCommand('foo')).toBe('foo')
})

describe('Approximate inputs', () => {
const value = 'foo'
it.each([
['Change la bordure en vert', 'Change la bordure en verre', value],
['Change la bordure en vert', 'Change la bordure en verres', value],
['Change la bordure en vert', 'Change la bordure en vers', value],
['Change la bordure en vert', 'Change la bordure en vairs', value],
['Change la bordure en vert', 'Changez la bordure en verre', value],
['Change la bordure en vert', 'Changez la bodure en verre', null],
['Change la bordure en vert', 'Change la bordure en rouge', null],
['Change la bordure en vert', 'Change la bordure en verre de rouge', null],
['Change la bordure en vert', 'Change la bordure en violet', null],
['Change la bordure en vert', 'Modifie la bordure en violet', null],
])('triggers callback mapped to approximate inputs', (command, input, expected) => {
const commands = {
[command]: () => value,
}
const {
result: { current: triggerCommand },
} = renderHook(() => useCommands(commands))
expect(triggerCommand(input)).toBe(expected)
})
})

it('returns null as no command is mapped to the input', () => {
const commands = {
foo: () => 'bar',
}
const {
result: { current: triggerCommand },
} = renderHook(() => useCommands(commands))
expect(triggerCommand('gag')).toBeNull()
})
})
21 changes: 21 additions & 0 deletions src/hooks/useCommands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Fuse from 'fuse.js'

const useCommands = (commands, precision = 0.4) => {
commands = !!commands
? Object.entries(commands)?.reduce((acc, [key, value]) => ({ [key.toLowerCase()]: value }), {})
: {}

const triggerCommand = (input) => {
const fuse = new Fuse(Object.keys(commands), { includeScore: true, ignoreLocation: true })
const result = fuse.search(input).filter((r) => r.score < precision)
if (!!result?.length) {
const key = result[0].item.toLowerCase()
return commands[key]?.(input)
}
return null
}

return triggerCommand
}

export default useCommands
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4219,6 +4219,11 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==

fuse.js@^6.4.6:
version "6.4.6"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.4.6.tgz#62f216c110e5aa22486aff20be7896d19a059b79"
integrity sha512-/gYxR/0VpXmWSfZOIPS3rWwU8SHgsRTwWuXhyb2O6s7aRuVtHtxCkR33bNYu3wyLyNx/Wpv0vU7FZy8Vj53VNw==

gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
Expand Down

0 comments on commit 12e086d

Please sign in to comment.