@untemps/react-vocal 2.0.0-beta.6 → 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.
- package/CHANGELOG.md +2 -0
- package/package.json +7 -1
- 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 -66
- package/dev/vite.config.js +0 -10
- package/dev/yarn.lock +0 -325
- package/dist/index.es.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.umd.js.map +0 -1
- package/src/components/Icon.jsx +0 -24
- package/src/components/Vocal.jsx +0 -261
- package/src/components/__tests__/Icon.test.jsx +0 -38
- package/src/components/__tests__/Vocal.test.jsx +0 -748
- 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 -115
- package/src/hooks/__tests__/useTimeout.test.js +0 -69
- package/src/hooks/__tests__/useVocal.test.js +0 -207
- package/src/hooks/useCommands.js +0 -75
- 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 -36
- package/vitest.setup.js +0 -83
|
@@ -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
|
-
aria-pressed="false"
|
|
8
|
-
data-testid="__vocal-root__"
|
|
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,115 +0,0 @@
|
|
|
1
|
-
import { act, renderHook } from '@testing-library/react'
|
|
2
|
-
|
|
3
|
-
import useCommands from '../useCommands'
|
|
4
|
-
|
|
5
|
-
// Static import anchors fuse.js in the module graph so vi.mock intercepts the
|
|
6
|
-
// dynamic import('fuse.js') inside the hook. Without it the dynamic import resolves
|
|
7
|
-
// against the real module before the async mock factory completes.
|
|
8
|
-
import 'fuse.js'
|
|
9
|
-
|
|
10
|
-
vi.mock('fuse.js', async () => {
|
|
11
|
-
const actual = await vi.importActual('fuse.js')
|
|
12
|
-
return actual
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
describe('useCommands', () => {
|
|
16
|
-
it('returns triggerCommand function', () => {
|
|
17
|
-
const triggerCommand = renderHook(() => useCommands())
|
|
18
|
-
expect(triggerCommand).toBeDefined()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('triggers callback mapped to the exact input', () => {
|
|
22
|
-
const commands = {
|
|
23
|
-
foo: () => 'bar',
|
|
24
|
-
}
|
|
25
|
-
const {
|
|
26
|
-
result: { current: triggerCommand },
|
|
27
|
-
} = renderHook(() => useCommands(commands))
|
|
28
|
-
expect(triggerCommand('foo')).toBe('bar')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('passes input as callback argument', () => {
|
|
32
|
-
const commands = {
|
|
33
|
-
foo: (input) => input,
|
|
34
|
-
}
|
|
35
|
-
const {
|
|
36
|
-
result: { current: triggerCommand },
|
|
37
|
-
} = renderHook(() => useCommands(commands))
|
|
38
|
-
expect(triggerCommand('foo')).toBe('foo')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('Approximate inputs', () => {
|
|
42
|
-
const value = 'foo'
|
|
43
|
-
it.each([
|
|
44
|
-
['Change la bordure en vert', 'Change la bordure en verre', value],
|
|
45
|
-
['Change la bordure en vert', 'Change la bordure en verres', value],
|
|
46
|
-
['Change la bordure en vert', 'Change la bordure en vers', value],
|
|
47
|
-
['Change la bordure en vert', 'Change la bordure en vairs', value],
|
|
48
|
-
['Change la bordure en vert', 'Changez la bordure en verre', value],
|
|
49
|
-
['Change la bordure en vert', 'Changez la bodure en verre', null],
|
|
50
|
-
['Change la bordure en vert', 'Change la bordure en rouge', null],
|
|
51
|
-
['Change la bordure en vert', 'Change la bordure en verre de rouge', null],
|
|
52
|
-
['Change la bordure en vert', 'Change la bordure en violet', null],
|
|
53
|
-
['Change la bordure en vert', 'Modifie la bordure en violet', null],
|
|
54
|
-
])('triggers callback mapped to approximate inputs', async (command, input, expected) => {
|
|
55
|
-
const commands = {
|
|
56
|
-
[command]: () => value,
|
|
57
|
-
}
|
|
58
|
-
const {
|
|
59
|
-
result: { current: triggerCommand },
|
|
60
|
-
} = renderHook(() => useCommands(commands))
|
|
61
|
-
// Flush the dynamic import microtask
|
|
62
|
-
await act(async () => {})
|
|
63
|
-
expect(triggerCommand(input)).toBe(expected)
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('returns null as no command is mapped to the input', () => {
|
|
68
|
-
const commands = {
|
|
69
|
-
foo: () => 'bar',
|
|
70
|
-
}
|
|
71
|
-
const {
|
|
72
|
-
result: { current: triggerCommand },
|
|
73
|
-
} = renderHook(() => useCommands(commands))
|
|
74
|
-
expect(triggerCommand('gag')).toBeNull()
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('triggers all registered commands when multiple commands are defined', () => {
|
|
78
|
-
const commands = {
|
|
79
|
-
rouge: () => 'red',
|
|
80
|
-
bleu: () => 'blue',
|
|
81
|
-
jaune: () => 'yellow',
|
|
82
|
-
}
|
|
83
|
-
const {
|
|
84
|
-
result: { current: triggerCommand },
|
|
85
|
-
} = renderHook(() => useCommands(commands))
|
|
86
|
-
expect(triggerCommand('rouge')).toBe('red')
|
|
87
|
-
expect(triggerCommand('bleu')).toBe('blue')
|
|
88
|
-
expect(triggerCommand('jaune')).toBe('yellow')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('does not match near-homophones with strict precision — rely on maxAlternatives instead', () => {
|
|
92
|
-
const commands = { vert: () => 'green' }
|
|
93
|
-
const {
|
|
94
|
-
result: { current: triggerCommand },
|
|
95
|
-
} = renderHook(() => useCommands(commands))
|
|
96
|
-
// 'verre' scores 0.4 against 'vert' — not strictly < STRICT_PRECISION (0.4)
|
|
97
|
-
expect(triggerCommand('verre')).toBeNull()
|
|
98
|
-
// The engine surfaces 'vert' as a secondary alternative (score 0) — exact match
|
|
99
|
-
expect(triggerCommand('vert')).toBe('green')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('falls back to contains matching when fuse.js is not available', async () => {
|
|
103
|
-
vi.doMock('fuse.js', () => {
|
|
104
|
-
throw new Error('fuse.js not installed')
|
|
105
|
-
})
|
|
106
|
-
const { default: useCommandsWithoutFuse } = await import('../useCommands')
|
|
107
|
-
const commands = { 'change color': () => 'matched' }
|
|
108
|
-
const {
|
|
109
|
-
result: { current: triggerCommand },
|
|
110
|
-
} = renderHook(() => useCommandsWithoutFuse(commands))
|
|
111
|
-
await act(async () => {})
|
|
112
|
-
expect(triggerCommand('change color')).toBe('matched')
|
|
113
|
-
vi.doUnmock('fuse.js')
|
|
114
|
-
})
|
|
115
|
-
})
|
|
@@ -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
|
-
})
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { renderHook } from '@testing-library/react'
|
|
2
|
-
import { Vocal as SpeechRecognitionWrapper } from '@untemps/vocal'
|
|
3
|
-
|
|
4
|
-
import useVocal from '../useVocal'
|
|
5
|
-
|
|
6
|
-
vi.mock('@untemps/vocal')
|
|
7
|
-
|
|
8
|
-
describe('useVocal', () => {
|
|
9
|
-
const mockStart = vi.fn()
|
|
10
|
-
const mockStop = vi.fn()
|
|
11
|
-
const mockAbort = vi.fn()
|
|
12
|
-
const mockAddEventListener = vi.fn()
|
|
13
|
-
const mockRemoveEventListener = vi.fn()
|
|
14
|
-
const mockCleanup = vi.fn()
|
|
15
|
-
|
|
16
|
-
const mockIsSupported = vi.fn()
|
|
17
|
-
Object.defineProperty(SpeechRecognitionWrapper, 'isSupported', {
|
|
18
|
-
get: mockIsSupported,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
describe('with no SpeechRecognition support', () => {
|
|
22
|
-
beforeAll(() => {
|
|
23
|
-
mockIsSupported.mockReturnValue(false)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('cannot create SpeechRecognition instance', () => {
|
|
27
|
-
const {
|
|
28
|
-
result: {
|
|
29
|
-
current: [ref],
|
|
30
|
-
},
|
|
31
|
-
} = renderHook(() => useVocal())
|
|
32
|
-
expect(ref.current).toBeNull()
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('not triggers start function', () => {
|
|
36
|
-
const {
|
|
37
|
-
result: {
|
|
38
|
-
current: [, { start }],
|
|
39
|
-
},
|
|
40
|
-
} = renderHook(() => useVocal())
|
|
41
|
-
start()
|
|
42
|
-
expect(mockStart).not.toHaveBeenCalled()
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('not triggers stop function', () => {
|
|
46
|
-
const {
|
|
47
|
-
result: {
|
|
48
|
-
current: [, { stop }],
|
|
49
|
-
},
|
|
50
|
-
} = renderHook(() => useVocal())
|
|
51
|
-
stop()
|
|
52
|
-
expect(mockStop).not.toHaveBeenCalled()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('not triggers abort function', () => {
|
|
56
|
-
const {
|
|
57
|
-
result: {
|
|
58
|
-
current: [, { abort }],
|
|
59
|
-
},
|
|
60
|
-
} = renderHook(() => useVocal())
|
|
61
|
-
abort()
|
|
62
|
-
expect(mockAbort).not.toHaveBeenCalled()
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('not triggers clean function', () => {
|
|
66
|
-
const {
|
|
67
|
-
result: {
|
|
68
|
-
current: [, { clean }],
|
|
69
|
-
},
|
|
70
|
-
} = renderHook(() => useVocal())
|
|
71
|
-
clean()
|
|
72
|
-
expect(mockCleanup).not.toHaveBeenCalled()
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('not triggers subscribe function', () => {
|
|
76
|
-
const {
|
|
77
|
-
result: {
|
|
78
|
-
current: [, { subscribe }],
|
|
79
|
-
},
|
|
80
|
-
} = renderHook(() => useVocal())
|
|
81
|
-
subscribe('foo', vi.fn())
|
|
82
|
-
expect(mockAddEventListener).not.toHaveBeenCalled()
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('not triggers unsubscribe function', () => {
|
|
86
|
-
const {
|
|
87
|
-
result: {
|
|
88
|
-
current: [, { unsubscribe }],
|
|
89
|
-
},
|
|
90
|
-
} = renderHook(() => useVocal())
|
|
91
|
-
unsubscribe('foo', vi.fn())
|
|
92
|
-
expect(mockRemoveEventListener).not.toHaveBeenCalled()
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
describe('with SpeechRecognition support', () => {
|
|
97
|
-
beforeAll(() => {
|
|
98
|
-
mockIsSupported.mockReturnValue(true)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
beforeEach(() => {
|
|
102
|
-
SpeechRecognitionWrapper.mockImplementation(function () {
|
|
103
|
-
return {
|
|
104
|
-
start: mockStart,
|
|
105
|
-
stop: mockStop,
|
|
106
|
-
abort: mockAbort,
|
|
107
|
-
addEventListener: mockAddEventListener,
|
|
108
|
-
removeEventListener: mockRemoveEventListener,
|
|
109
|
-
cleanup: mockCleanup,
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
afterEach(() => {
|
|
115
|
-
SpeechRecognitionWrapper.mockReset()
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('creates SpeechRecognition instance', () => {
|
|
119
|
-
const {
|
|
120
|
-
result: {
|
|
121
|
-
current: [ref],
|
|
122
|
-
},
|
|
123
|
-
} = renderHook(() => useVocal())
|
|
124
|
-
expect(ref.current).toBeDefined()
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('passes maxAlternatives to SpeechRecognitionWrapper constructor', () => {
|
|
128
|
-
renderHook(() => useVocal('en-US', null, 5))
|
|
129
|
-
expect(SpeechRecognitionWrapper).toHaveBeenCalledWith({
|
|
130
|
-
lang: 'en-US',
|
|
131
|
-
grammars: null,
|
|
132
|
-
maxAlternatives: 5,
|
|
133
|
-
continuous: false,
|
|
134
|
-
})
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('uses custom SpeechRecognition instance', () => {
|
|
138
|
-
const foo = new SpeechRecognitionWrapper()
|
|
139
|
-
const {
|
|
140
|
-
result: {
|
|
141
|
-
current: [ref],
|
|
142
|
-
},
|
|
143
|
-
} = renderHook(() => useVocal(null, null, 1, false, foo))
|
|
144
|
-
expect(ref.current).toBe(foo)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('triggers start function', () => {
|
|
148
|
-
const {
|
|
149
|
-
result: {
|
|
150
|
-
current: [, { start }],
|
|
151
|
-
},
|
|
152
|
-
} = renderHook(() => useVocal())
|
|
153
|
-
start()
|
|
154
|
-
expect(mockStart).toHaveBeenCalled()
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
it('triggers stop function', () => {
|
|
158
|
-
const {
|
|
159
|
-
result: {
|
|
160
|
-
current: [, { stop }],
|
|
161
|
-
},
|
|
162
|
-
} = renderHook(() => useVocal())
|
|
163
|
-
stop()
|
|
164
|
-
expect(mockStop).toHaveBeenCalled()
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('triggers abort function', () => {
|
|
168
|
-
const {
|
|
169
|
-
result: {
|
|
170
|
-
current: [, { abort }],
|
|
171
|
-
},
|
|
172
|
-
} = renderHook(() => useVocal())
|
|
173
|
-
abort()
|
|
174
|
-
expect(mockAbort).toHaveBeenCalled()
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('triggers clean function', () => {
|
|
178
|
-
const {
|
|
179
|
-
result: {
|
|
180
|
-
current: [, { clean }],
|
|
181
|
-
},
|
|
182
|
-
} = renderHook(() => useVocal())
|
|
183
|
-
clean()
|
|
184
|
-
expect(mockCleanup).toHaveBeenCalled()
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('triggers subscribe function', () => {
|
|
188
|
-
const {
|
|
189
|
-
result: {
|
|
190
|
-
current: [, { subscribe }],
|
|
191
|
-
},
|
|
192
|
-
} = renderHook(() => useVocal())
|
|
193
|
-
subscribe('foo', vi.fn())
|
|
194
|
-
expect(mockAddEventListener).toHaveBeenCalled()
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('triggers unsubscribe function', () => {
|
|
198
|
-
const {
|
|
199
|
-
result: {
|
|
200
|
-
current: [, { unsubscribe }],
|
|
201
|
-
},
|
|
202
|
-
} = renderHook(() => useVocal())
|
|
203
|
-
unsubscribe('foo', vi.fn())
|
|
204
|
-
expect(mockRemoveEventListener).toHaveBeenCalled()
|
|
205
|
-
})
|
|
206
|
-
})
|
|
207
|
-
})
|
package/src/hooks/useCommands.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef } from 'react'
|
|
2
|
-
|
|
3
|
-
const useCommands = (commands, precision = 0.4) => {
|
|
4
|
-
const normalized = useMemo(
|
|
5
|
-
() =>
|
|
6
|
-
!!commands
|
|
7
|
-
? Object.entries(commands).reduce((acc, [key, value]) => ({ ...acc, [key.toLowerCase()]: value }), {})
|
|
8
|
-
: {},
|
|
9
|
-
[commands]
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
const keys = useMemo(() => Object.keys(normalized), [normalized])
|
|
13
|
-
|
|
14
|
-
// Fuzzy matching is only needed for phrase command keys.
|
|
15
|
-
// Single-word keys use exact case-insensitive lookup — simpler and no false positives.
|
|
16
|
-
const hasPhraseKeys = useMemo(() => keys.some((k) => k.includes(' ')), [keys])
|
|
17
|
-
|
|
18
|
-
// Lazy-loaded so consumers using only single-word commands incur no bundle cost.
|
|
19
|
-
const fuseRef = useRef(null)
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (!hasPhraseKeys) {
|
|
23
|
-
fuseRef.current = null
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
import('fuse.js')
|
|
27
|
-
.then((module) => {
|
|
28
|
-
const Fuse = module.default ?? module
|
|
29
|
-
fuseRef.current = new Fuse(keys, { includeScore: true, ignoreLocation: true })
|
|
30
|
-
})
|
|
31
|
-
.catch(() => {
|
|
32
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
33
|
-
console.warn(
|
|
34
|
-
'[react-vocal] fuse.js is not installed. Phrase command keys will fall back to exact matching. ' +
|
|
35
|
-
'Install fuse.js to enable fuzzy matching: npm install fuse.js'
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
})
|
|
39
|
-
}, [hasPhraseKeys, keys])
|
|
40
|
-
|
|
41
|
-
const triggerCommand = (rawInput) => {
|
|
42
|
-
if (!keys.length) return null
|
|
43
|
-
|
|
44
|
-
if (!hasPhraseKeys) {
|
|
45
|
-
const words = rawInput.trim().split(/\s+/)
|
|
46
|
-
const targets = words.length > 1 ? words : [rawInput.trim()]
|
|
47
|
-
for (const w of targets) {
|
|
48
|
-
const commandKey = w.toLowerCase()
|
|
49
|
-
if (commandKey in normalized) return normalized[commandKey]?.(w, commandKey)
|
|
50
|
-
}
|
|
51
|
-
return null
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const fuse = fuseRef.current
|
|
55
|
-
if (fuse) {
|
|
56
|
-
const result = fuse.search(rawInput).filter((r) => r.score < precision)
|
|
57
|
-
if (result?.length) {
|
|
58
|
-
const commandKey = result[0].item.toLowerCase()
|
|
59
|
-
return normalized[commandKey]?.(rawInput, commandKey)
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
// `k.includes(lInput)` can produce false positives when input is short
|
|
63
|
-
// (e.g. "rouge" matches "change en rouge"). Accepted tradeoff: this branch
|
|
64
|
-
// only runs when fuse.js is absent, so degraded precision is expected.
|
|
65
|
-
const lInput = rawInput.toLowerCase()
|
|
66
|
-
const commandKey = keys.find((k) => lInput.includes(k) || k.includes(lInput))
|
|
67
|
-
if (commandKey) return normalized[commandKey]?.(rawInput, commandKey)
|
|
68
|
-
}
|
|
69
|
-
return null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return triggerCommand
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export default useCommands
|
package/src/hooks/useTimeout.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from 'react'
|
|
2
|
-
|
|
3
|
-
const useTimeout = (handler, timeout = 0) => {
|
|
4
|
-
const ref = useRef(-1)
|
|
5
|
-
|
|
6
|
-
const stop = useCallback(() => {
|
|
7
|
-
clearTimeout(ref.current)
|
|
8
|
-
ref.current = -1
|
|
9
|
-
}, [])
|
|
10
|
-
|
|
11
|
-
const start = useCallback(() => {
|
|
12
|
-
stop()
|
|
13
|
-
ref.current = setTimeout(handler, timeout)
|
|
14
|
-
}, [handler, timeout, stop])
|
|
15
|
-
|
|
16
|
-
useEffect(() => stop, [stop])
|
|
17
|
-
|
|
18
|
-
return [start, stop]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export default useTimeout
|
package/src/hooks/useVocal.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from 'react'
|
|
2
|
-
import { Vocal as SpeechRecognitionWrapper } from '@untemps/vocal'
|
|
3
|
-
|
|
4
|
-
const useVocal = (lang = 'en-US', grammars = null, maxAlternatives = 1, continuous = false, __rsInstance = null) => {
|
|
5
|
-
const ref = useRef(null)
|
|
6
|
-
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
if (SpeechRecognitionWrapper.isSupported) {
|
|
9
|
-
ref.current = __rsInstance || new SpeechRecognitionWrapper({ lang, grammars, maxAlternatives, continuous })
|
|
10
|
-
return () => {
|
|
11
|
-
ref.current.abort()
|
|
12
|
-
ref.current.cleanup()
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}, [lang, grammars, maxAlternatives, continuous, __rsInstance])
|
|
16
|
-
|
|
17
|
-
const start = useCallback(() => {
|
|
18
|
-
if (ref.current) {
|
|
19
|
-
ref.current.start()
|
|
20
|
-
}
|
|
21
|
-
}, [])
|
|
22
|
-
|
|
23
|
-
const stop = useCallback(() => {
|
|
24
|
-
if (ref.current) {
|
|
25
|
-
ref.current.stop()
|
|
26
|
-
}
|
|
27
|
-
}, [])
|
|
28
|
-
|
|
29
|
-
const abort = useCallback(() => {
|
|
30
|
-
if (ref.current) {
|
|
31
|
-
ref.current.abort()
|
|
32
|
-
}
|
|
33
|
-
}, [])
|
|
34
|
-
|
|
35
|
-
const subscribe = useCallback((eventType, handler) => {
|
|
36
|
-
if (ref.current) {
|
|
37
|
-
ref.current.addEventListener(eventType, handler)
|
|
38
|
-
}
|
|
39
|
-
}, [])
|
|
40
|
-
|
|
41
|
-
const unsubscribe = useCallback((eventType, handler) => {
|
|
42
|
-
if (ref.current) {
|
|
43
|
-
ref.current.removeEventListener(eventType, handler)
|
|
44
|
-
}
|
|
45
|
-
}, [])
|
|
46
|
-
|
|
47
|
-
const clean = useCallback(() => {
|
|
48
|
-
if (ref.current) {
|
|
49
|
-
ref.current.cleanup()
|
|
50
|
-
}
|
|
51
|
-
}, [])
|
|
52
|
-
|
|
53
|
-
return [ref, { start, stop, abort, subscribe, unsubscribe, clean }]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export default useVocal
|
package/src/index.js
DELETED
package/vite.config.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vite'
|
|
2
|
-
import react from '@vitejs/plugin-react'
|
|
3
|
-
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
plugins: [react()],
|
|
6
|
-
build: {
|
|
7
|
-
lib: {
|
|
8
|
-
entry: 'src/index.js',
|
|
9
|
-
name: 'ReactVocal',
|
|
10
|
-
formats: ['es', 'cjs', 'umd'],
|
|
11
|
-
fileName: (format) => ({ es: 'index.es.js', umd: 'index.umd.js', cjs: 'index.js' })[format],
|
|
12
|
-
},
|
|
13
|
-
rollupOptions: {
|
|
14
|
-
external: ['react', 'react-dom', 'fuse.js'],
|
|
15
|
-
output: {
|
|
16
|
-
globals: {
|
|
17
|
-
react: 'React',
|
|
18
|
-
'react-dom': 'ReactDOM',
|
|
19
|
-
'fuse.js': 'Fuse',
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
sourcemap: true,
|
|
24
|
-
},
|
|
25
|
-
test: {
|
|
26
|
-
globals: true,
|
|
27
|
-
environment: 'jsdom',
|
|
28
|
-
setupFiles: ['./vitest.setup.js'],
|
|
29
|
-
restoreMocks: true,
|
|
30
|
-
coverage: {
|
|
31
|
-
provider: 'v8',
|
|
32
|
-
reporter: ['text', 'lcov'],
|
|
33
|
-
reportsDirectory: './coverage',
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
})
|